diff --git a/data_migration_tools/package-lock.json b/data_migration_tools/package-lock.json new file mode 100644 index 0000000..343f405 --- /dev/null +++ b/data_migration_tools/package-lock.json @@ -0,0 +1,63 @@ +{ + "name": "data_migration_tools", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "author": "", + "description": "", + "license": "UNLICENSED", + "private": true, + "packages": {}, + "devDependencies": { + "@types/express": "^4.17.17", + "@types/multer": "^1.4.7", + "@types/node": "^20.2.3", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "28.0.3", + "license-checker": "^25.0.1", + "prettier": "^2.3.2", + "source-map-support": "^0.5.20", + "supertest": "^6.1.3", + "swagger-ui-express": "^4.5.0", + "ts-jest": "28.0.1", + "ts-loader": "^9.2.3", + "ts-node": "^10.0.0", + "tsconfig-paths": "4.0.0", + "typescript": "^4.3.5" + }, + "jest": { + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testEnvironment": "node", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } + }, + "scripts": { + "build": "nest build && cp -r build dist", + "build:exe": "nest build && cp -r build dist && sh ./buildTool.sh", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", + "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "prebuild": "rimraf dist", + "start": "nest start", + "start:debug": "nest start --debug --watch", + "start:dev": "nest start --watch", + "start:prod": "node dist/main", + "test": "jest", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:watch": "jest --watch" + } +} diff --git a/data_migration_tools/server/.env.local.example b/data_migration_tools/server/.env.local.example index e69de29..0cee3f5 100644 --- a/data_migration_tools/server/.env.local.example +++ b/data_migration_tools/server/.env.local.example @@ -0,0 +1,36 @@ +STAGE=local +NO_COLOR=TRUE +CORS=TRUE +PORT=8280 +# 開発環境ではADB2Cが別テナントになる都合上、環境変数を分けている +TENANT_NAME=adb2codmsdev +SIGNIN_FLOW_NAME=b2c_1_signin_dev +ADB2C_TENANT_ID=xxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx +ADB2C_CLIENT_SECRET=xxxxxxxx +ADB2C_ORIGIN=https://zzzzzzzzzz +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n" +SENDGRID_API_KEY=xxxxxxxxxxxxxxxx +MAIL_FROM=xxxxx@xxxxx.xxxx +NOTIFICATION_HUB_NAME=ntf-odms-dev +NOTIFICATION_HUB_CONNECT_STRING=XXXXXXXXXXXXXXXXXX +APP_DOMAIN=http://localhost:8081/ +STORAGE_TOKEN_EXPIRE_TIME=2 +STORAGE_ACCOUNT_NAME_US=saodmsusdev +STORAGE_ACCOUNT_NAME_AU=saodmsaudev +STORAGE_ACCOUNT_NAME_EU=saodmseudev +STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX +STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX +STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX +STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA +STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA +STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA +ACCESS_TOKEN_LIFETIME_WEB=7200 +REFRESH_TOKEN_LIFETIME_WEB=86400 +REFRESH_TOKEN_LIFETIME_DEFAULT=2592000 +EMAIL_CONFIRM_LIFETIME=86400 +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass +ADB2C_CACHE_TTL=86400 \ No newline at end of file diff --git a/data_migration_tools/server/package-lock.json b/data_migration_tools/server/package-lock.json index 50d9f33..c1c2143 100644 --- a/data_migration_tools/server/package-lock.json +++ b/data_migration_tools/server/package-lock.json @@ -27,10 +27,11 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "multer": "^1.4.5-lts.1", - "mysql2": "^3.9.1", + "mysql2": "^2.3.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "swagger-cli": "^4.0.4" + "swagger-cli": "^4.0.4", + "typeorm": "^0.3.20" }, "devDependencies": { "@types/express": "^4.17.17", @@ -52,15 +53,6 @@ "typescript": "^4.3.5" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -1224,9 +1216,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", + "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" @@ -1239,23 +1231,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", + "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", + "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", + "espree": "^9.5.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -1311,22 +1303,22 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", + "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", "minimatch": "^3.0.5" }, "engines": { @@ -1347,16 +1339,15 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "peer": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1373,7 +1364,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "peer": true, "engines": { "node": ">=12" }, @@ -1385,7 +1375,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "peer": true, "engines": { "node": ">=12" }, @@ -1396,14 +1385,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "peer": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "peer": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1420,7 +1407,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "peer": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1435,7 +1421,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "peer": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2576,7 +2561,6 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true, - "peer": true, "engines": { "node": ">=14" } @@ -2608,8 +2592,7 @@ "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", - "peer": true + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@tootallnate/once": { "version": "2.0.0", @@ -2802,9 +2785,9 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true, "peer": true }, @@ -2922,12 +2905,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vercel/ncc": { "version": "0.36.1", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.36.1.tgz", @@ -3258,8 +3235,7 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "peer": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "node_modules/anymatch": { "version": "3.1.3", @@ -3278,7 +3254,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "peer": true, "engines": { "node": ">= 6.0.0" } @@ -3773,7 +3748,6 @@ "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "peer": true, "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", @@ -4093,8 +4067,7 @@ "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "peer": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "4.3.4", @@ -4272,8 +4245,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "peer": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/easy-table": { "version": "1.1.0", @@ -4382,28 +4354,27 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", + "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.1", + "@eslint/js": "8.36.0", + "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.5.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4411,19 +4382,22 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "graphemer": "^1.4.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", + "optionator": "^0.9.1", "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -4484,15 +4458,12 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ajv": { @@ -4518,9 +4489,9 @@ "dev": true }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4528,9 +4499,6 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/estraverse": { @@ -4573,14 +4541,14 @@ "dev": true }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", + "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5050,7 +5018,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "peer": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -5066,7 +5033,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "peer": true, "engines": { "node": ">=14" }, @@ -5242,9 +5208,9 @@ "peer": true }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -5261,10 +5227,10 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, "node_modules/has": { @@ -5310,7 +5276,6 @@ "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "peer": true, "engines": { "node": "*" } @@ -5760,7 +5725,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "peer": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -6723,6 +6687,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7097,9 +7071,9 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -7247,7 +7221,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "peer": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -7291,16 +7264,16 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "node_modules/mysql2": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz", - "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", "dependencies": { - "denque": "^2.1.0", + "denque": "^2.0.1", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru-cache": "^8.0.0", - "named-placeholders": "^1.1.3", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" }, @@ -7320,18 +7293,25 @@ } }, "node_modules/mysql2/node_modules/lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=16.14" + "node": ">=10" } }, + "node_modules/mysql2/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "peer": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -7542,17 +7522,17 @@ "peer": true }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" }, "engines": { "node": ">= 0.8.0" @@ -7678,14 +7658,12 @@ "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "peer": true + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "peer": true, "dependencies": { "parse5": "^6.0.1" } @@ -7693,8 +7671,7 @@ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "peer": true + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "node_modules/parseurl": { "version": "1.3.3", @@ -7738,7 +7715,6 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "peer": true, "dependencies": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -7754,7 +7730,6 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "peer": true, "engines": { "node": "14 || >=16.14" } @@ -8475,7 +8450,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "peer": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -8732,7 +8706,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8758,7 +8731,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -9069,7 +9041,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "peer": true, "dependencies": { "any-promise": "^1.0.0" } @@ -9078,7 +9049,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "peer": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -9351,7 +9321,6 @@ "version": "0.3.20", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "app-root-path": "^3.1.0", @@ -9457,7 +9426,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -9480,7 +9448,6 @@ "url": "https://feross.org/support" } ], - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -9490,7 +9457,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -9504,7 +9470,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "peer": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -9526,7 +9491,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9541,7 +9505,6 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "peer": true, "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -9555,14 +9518,12 @@ "node_modules/typeorm/node_modules/reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", - "peer": true + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" }, "node_modules/typeorm/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -9580,7 +9541,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "peer": true, "engines": { "node": ">=12" } @@ -9854,6 +9814,15 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9875,7 +9844,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9996,12 +9964,6 @@ } }, "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -10916,29 +10878,29 @@ } }, "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", + "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" } }, "@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.4.0.tgz", + "integrity": "sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==", "dev": true }, "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.1.tgz", + "integrity": "sha512-eFRmABvW2E5Ho6f5fHLqgena46rOj7r7OKHYfLElqcBfGFHHpjBhivyi5+jOEQuSpdc/1phIZJlbC2te+tZNIw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", + "espree": "^9.5.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -10983,19 +10945,19 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.36.0.tgz", + "integrity": "sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", "minimatch": "^3.0.5" } }, @@ -11006,16 +10968,15 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "peer": true, "requires": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -11028,26 +10989,22 @@ "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "peer": true + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" }, "ansi-styles": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "peer": true + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "peer": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "peer": true, "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -11058,7 +11015,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "peer": true, "requires": { "ansi-regex": "^6.0.1" } @@ -11067,7 +11023,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "peer": true, "requires": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -11844,8 +11799,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "peer": true + "optional": true }, "@sinclair/typebox": { "version": "0.24.51", @@ -11874,8 +11828,7 @@ "@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", - "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", - "peer": true + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@tootallnate/once": { "version": "2.0.0", @@ -12065,9 +12018,9 @@ } }, "@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true, "peer": true }, @@ -12184,12 +12137,6 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "@vercel/ncc": { "version": "0.36.1", "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.36.1.tgz", @@ -12468,8 +12415,7 @@ "any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "peer": true + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" }, "anymatch": { "version": "3.1.3", @@ -12484,8 +12430,7 @@ "app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", - "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", - "peer": true + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==" }, "append-field": { "version": "1.0.0", @@ -12851,7 +12796,6 @@ "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", - "peer": true, "requires": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", @@ -13094,8 +13038,7 @@ "dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "peer": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "debug": { "version": "4.3.4", @@ -13219,8 +13162,7 @@ "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "peer": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "easy-table": { "version": "1.1.0", @@ -13308,28 +13250,27 @@ "dev": true }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", + "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.1", + "@eslint/js": "8.36.0", + "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.5.0", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -13337,19 +13278,22 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "graphemer": "^1.4.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", + "optionator": "^0.9.1", "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -13372,9 +13316,9 @@ "dev": true }, "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -13441,20 +13385,20 @@ } }, "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true }, "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.0.tgz", + "integrity": "sha512-JPbJGhKc47++oo4JkEoTe2wjy4fmMwvFpgJT9cQzmfXKp22Dr6Hf1tdCteLz1h0P3t+mGvWZ+4Uankvh8+c6zw==", "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^3.3.0" } }, "esprima": { @@ -13822,7 +13766,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "peer": true, "requires": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -13831,8 +13774,7 @@ "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "peer": true + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" } } }, @@ -13961,9 +13903,9 @@ "peer": true }, "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -13974,10 +13916,10 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, "has": { @@ -14007,8 +13949,7 @@ "highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", - "peer": true + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, "hosted-git-info": { "version": "2.8.9", @@ -14330,7 +14271,6 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "peer": true, "requires": { "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" @@ -15077,6 +15017,12 @@ } } }, + "js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15389,9 +15335,9 @@ } }, "long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "lru-cache": { "version": "5.1.1", @@ -15504,8 +15450,7 @@ "minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "peer": true + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==" }, "mkdirp": { "version": "0.5.6", @@ -15540,16 +15485,16 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "mysql2": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.1.tgz", - "integrity": "sha512-3njoWAAhGBYy0tWBabqUQcLtczZUxrmmtc2vszQUekg3kTJyZ5/IeLC3Fo04u6y6Iy5Sba7pIIa2P/gs8D3ZeQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", + "integrity": "sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA==", "requires": { - "denque": "^2.1.0", + "denque": "^2.0.1", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", - "long": "^5.2.1", - "lru-cache": "^8.0.0", - "named-placeholders": "^1.1.3", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" }, @@ -15563,9 +15508,17 @@ } }, "lru-cache": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", - "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -15573,7 +15526,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "peer": true, "requires": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -15735,17 +15687,17 @@ "peer": true }, "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, "ora": { @@ -15832,14 +15784,12 @@ "parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", - "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "peer": true + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" }, "parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "peer": true, "requires": { "parse5": "^6.0.1" }, @@ -15847,8 +15797,7 @@ "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "peer": true + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" } } }, @@ -15882,7 +15831,6 @@ "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "peer": true, "requires": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -15891,8 +15839,7 @@ "lru-cache": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "peer": true + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" } } }, @@ -16439,7 +16386,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "peer": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -16655,7 +16601,6 @@ "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "peer": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -16674,7 +16619,6 @@ "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "peer": true, "requires": { "ansi-regex": "^5.0.1" } @@ -16886,7 +16830,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "peer": true, "requires": { "any-promise": "^1.0.0" } @@ -16895,7 +16838,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "peer": true, "requires": { "thenify": ">= 3.1.0 < 4" } @@ -17072,7 +17014,6 @@ "version": "0.3.20", "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.20.tgz", "integrity": "sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==", - "peer": true, "requires": { "@sqltools/formatter": "^1.2.5", "app-root-path": "^3.1.0", @@ -17095,7 +17036,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "peer": true, "requires": { "balanced-match": "^1.0.0" } @@ -17104,7 +17044,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "peer": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -17114,7 +17053,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "peer": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -17125,7 +17063,6 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "peer": true, "requires": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -17138,7 +17075,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "peer": true, "requires": { "brace-expansion": "^2.0.1" } @@ -17146,20 +17082,17 @@ "mkdirp": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "peer": true + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==" }, "reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", - "peer": true + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "peer": true, "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -17173,8 +17106,7 @@ "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "peer": true + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" } } }, @@ -17373,6 +17305,12 @@ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -17387,7 +17325,6 @@ "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "peer": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", diff --git a/data_migration_tools/server/package.json b/data_migration_tools/server/package.json index fee15e8..a2299cb 100644 --- a/data_migration_tools/server/package.json +++ b/data_migration_tools/server/package.json @@ -41,10 +41,11 @@ "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", "multer": "^1.4.5-lts.1", - "mysql2": "^3.9.1", + "mysql2": "^2.3.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "swagger-cli": "^4.0.4" + "swagger-cli": "^4.0.4", + "typeorm": "^0.3.20" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/data_migration_tools/server/src/app.module.ts b/data_migration_tools/server/src/app.module.ts index 23a4c76..6935bba 100644 --- a/data_migration_tools/server/src/app.module.ts +++ b/data_migration_tools/server/src/app.module.ts @@ -1,16 +1,32 @@ import { MiddlewareConsumer, Module } from "@nestjs/common"; import { ServeStaticModule } from "@nestjs/serve-static"; import { ConfigModule, ConfigService } from "@nestjs/config"; +import { TypeOrmModule } from "@nestjs/typeorm"; 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 { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module"; +import { RegisterController } from "./features/register/register.controller"; +import { RegisterService } from "./features/register/register.service"; +import { RegisterModule } from "./features/register/register.module"; +import { AccountsRepositoryModule } from "./repositories/accounts/accounts.repository.module"; +import { UsersRepositoryModule } from "./repositories/users/users.repository.module"; +import { SortCriteriaRepositoryModule } from "./repositories/sort_criteria/sort_criteria.repository.module"; +import { LicensesRepositoryModule } from "./repositories/licenses/licenses.repository.module"; +import { WorktypesRepositoryModule } from "./repositories/worktypes/worktypes.repository.module"; +import { AccountsController } from "./features/accounts/accounts.controller"; +import { AccountsService } from "./features/accounts/accounts.service"; +import { AccountsModule } from "./features/accounts/accounts.module"; +import { UsersController } from "./features/users/users.controller"; +import { UsersService } from "./features/users/users.service"; +import { UsersModule } from "./features/users/users.module"; +import { DeleteModule } from "./features/delete/delete.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"; - +import { TransferModule } from "./features/transfer/transfer.module"; +import { TransferController } from "./features/transfer/transfer.controller"; +import { TransferService } from "./features/transfer/transfer.service"; @Module({ imports: [ ServeStaticModule.forRoot({ @@ -20,6 +36,19 @@ import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module"; envFilePath: [".env.local", ".env"], isGlobal: true, }), + AdB2cModule, + AccountsModule, + UsersModule, + TransferModule, + RegisterModule, + AccountsRepositoryModule, + UsersRepositoryModule, + SortCriteriaRepositoryModule, + LicensesRepositoryModule, + WorktypesRepositoryModule, + BlobstorageModule, + DeleteModule, + DeleteRepositoryModule, TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ @@ -34,13 +63,21 @@ import { BlobstorageModule } from "./gateways/blobstorage/blobstorage.module"; }), inject: [ConfigService], }), - DeleteModule, - AdB2cModule, - BlobstorageModule, - DeleteRepositoryModule, ], - controllers: [DeleteController], - providers: [DeleteService], + controllers: [ + RegisterController, + AccountsController, + UsersController, + DeleteController, + TransferController, + ], + providers: [ + RegisterService, + AccountsService, + UsersService, + DeleteService, + TransferService, + ], }) export class AppModule { configure(consumer: MiddlewareConsumer) { diff --git a/data_migration_tools/server/src/common/error/code.ts b/data_migration_tools/server/src/common/error/code.ts new file mode 100644 index 0000000..3c488da --- /dev/null +++ b/data_migration_tools/server/src/common/error/code.ts @@ -0,0 +1,70 @@ +/* +エラーコード作成方針 +E+6桁(数字)で構成する。 +- 1~2桁目の値は種類(業務エラー、システムエラー...) +- 3~4桁目の値は原因箇所(トークン、DB、...) +- 5~6桁目の値は任意の重複しない値 +ex) +E00XXXX : システムエラー(通信エラーやDB接続失敗など) +E01XXXX : 業務エラー +EXX00XX : 内部エラー(内部プログラムのエラー) +EXX01XX : トークンエラー(トークン認証関連) +EXX02XX : DBエラー(DB関連) +EXX03XX : ADB2Cエラー(DB関連) +*/ +export const ErrorCodes = [ + 'E009999', // 汎用エラー + 'E000101', // トークン形式不正エラー + 'E000102', // トークン有効期限切れエラー + 'E000103', // トークン非アクティブエラー + 'E000104', // トークン署名エラー + 'E000105', // トークン発行元エラー + 'E000106', // トークンアルゴリズムエラー + 'E000107', // トークン不足エラー + 'E000108', // トークン権限エラー + 'E000301', // ADB2Cへのリクエスト上限超過エラー + 'E000401', // IPアドレス未設定エラー + 'E000501', // リクエストID未設定エラー + 'E010001', // パラメータ形式不正エラー + 'E010201', // 未認証ユーザエラー + 'E010202', // 認証済ユーザエラー + 'E010203', // 管理ユーザ権限エラー + 'E010204', // ユーザ不在エラー + 'E010205', // DBのRoleが想定外の値エラー + 'E010206', // DBのTierが想定外の値エラー + 'E010207', // ユーザーのRole変更不可エラー + 'E010208', // ユーザーの暗号化パスワード不足エラー + 'E010209', // ユーザーの同意済み利用規約バージョンが最新でないエラー + 'E010301', // メールアドレス登録済みエラー + 'E010302', // authorId重複エラー + 'E010401', // PONumber重複エラー + 'E010501', // アカウント不在エラー + 'E010502', // アカウント情報変更不可エラー + 'E010503', // 代行操作不許可エラー + 'E010504', // アカウントロックエラー + 'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) + 'E010602', // タスク変更権限不足エラー + 'E010603', // タスク不在エラー + 'E010701', // Blobファイル不在エラー + 'E010801', // ライセンス不在エラー + 'E010802', // ライセンス取り込み済みエラー + 'E010803', // ライセンス発行済みエラー + 'E010804', // ライセンス不足エラー + 'E010805', // ライセンス有効期限切れエラー + 'E010806', // ライセンス割り当て不可エラー + 'E010807', // ライセンス割り当て解除済みエラー + 'E010808', // ライセンス注文キャンセル不可エラー + 'E010809', // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) + 'E010810', // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) + 'E010811', // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) + 'E010812', // ライセンス未割当エラー + 'E010908', // タイピストグループ不在エラー + 'E010909', // タイピストグループ名重複エラー + 'E011001', // ワークタイプ重複エラー + 'E011002', // ワークタイプ登録上限超過エラー + 'E011003', // ワークタイプ不在エラー + 'E011004', // ワークタイプ使用中エラー + 'E012001', // テンプレートファイル不在エラー + 'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー + 'E013002', // ワークフロー不在エラー +] as const; diff --git a/data_migration_tools/server/src/common/error/makeErrorResponse.ts b/data_migration_tools/server/src/common/error/makeErrorResponse.ts new file mode 100644 index 0000000..0a677b4 --- /dev/null +++ b/data_migration_tools/server/src/common/error/makeErrorResponse.ts @@ -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, + }; +}; diff --git a/data_migration_tools/server/src/common/error/message.ts b/data_migration_tools/server/src/common/error/message.ts new file mode 100644 index 0000000..9383694 --- /dev/null +++ b/data_migration_tools/server/src/common/error/message.ts @@ -0,0 +1,59 @@ +import { Errors } from './types/types'; + +// エラーコードとメッセージ対応表 +export const errors: Errors = { + E009999: 'Internal Server Error.', + E000101: 'Token invalid format Error.', + E000102: 'Token expired Error.', + E000103: 'Token not before Error', + E000104: 'Token invalid signature Error.', + E000105: 'Token invalid issuer Error.', + E000106: 'Token invalid algorithm Error.', + E000107: 'Token is not exist Error.', + E000108: 'Token authority failed Error.', + E000301: 'ADB2C request limit exceeded Error', + E000401: 'IP address not found Error.', + E000501: 'Request ID not found Error.', + E010001: 'Param invalid format Error.', + E010201: 'Email not verified user Error.', + E010202: 'Email already verified user Error.', + E010203: 'Administrator Permissions Error.', + E010204: 'User not Found Error.', + E010205: 'Role from DB is unexpected value Error.', + E010206: 'Tier from DB is unexpected value Error.', + E010207: 'User role change not allowed Error.', + E010208: 'User encryption password not found Error.', + E010209: 'Accepted term not latest Error.', + E010301: 'This email user already created Error', + E010302: 'This AuthorId already used Error', + E010401: 'This PoNumber already used Error', + E010501: 'Account not Found Error.', + E010502: 'Account information cannot be changed Error.', + E010503: 'Delegation not allowed Error.', + E010504: 'Account is locked Error.', + E010601: 'Task is not Editable Error', + E010602: 'No task edit permissions Error', + E010603: 'Task not found Error.', + E010701: 'File not found in Blob Storage Error.', + E010801: 'License not exist Error', + E010802: 'License already activated Error', + E010803: 'License already issued Error', + E010804: 'License shortage Error', + E010805: 'License is expired Error', + E010806: 'License is unavailable Error', + E010807: 'License is already deallocated Error', + E010808: 'Order cancel failed Error', + E010809: 'Already license order status changed Error', + E010810: 'Cancellation period expired error', + E010811: 'Already license allocated Error', + E010812: 'License not allocated Error', + E010908: 'Typist Group not exist Error', + E010909: 'Typist Group name already exist Error', + E011001: 'This WorkTypeID already used Error', + E011002: 'WorkTypeID create limit exceeded Error', + E011003: 'WorkTypeID not found Error', + E011004: 'WorkTypeID is in use Error', + E012001: 'Template file not found Error', + E013001: 'AuthorId and WorktypeId pair already exists Error', + E013002: 'Workflow not found Error', +}; diff --git a/data_migration_tools/server/src/common/error/types/types.ts b/data_migration_tools/server/src/common/error/types/types.ts new file mode 100644 index 0000000..8746924 --- /dev/null +++ b/data_migration_tools/server/src/common/error/types/types.ts @@ -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; +}; diff --git a/data_migration_tools/server/src/common/errors/types/types.ts b/data_migration_tools/server/src/common/errors/types/types.ts index 8746924..f68eda7 100644 --- a/data_migration_tools/server/src/common/errors/types/types.ts +++ b/data_migration_tools/server/src/common/errors/types/types.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ErrorCodes } from '../code'; +import { ApiProperty } from "@nestjs/swagger"; +import { ErrorCodes } from "../code"; export class ErrorResponse { @ApiProperty() diff --git a/data_migration_tools/server/src/common/log/context.ts b/data_migration_tools/server/src/common/log/context.ts new file mode 100644 index 0000000..1b887c6 --- /dev/null +++ b/data_migration_tools/server/src/common/log/context.ts @@ -0,0 +1,32 @@ +import { Request } from 'express'; +import { Context } from './types'; + +export const makeContext = ( + externalId: string, + requestId: string, + delegationId?: string, +): Context => { + return new Context(externalId, requestId, delegationId); +}; + +// リクエストヘッダーからrequestIdを取得する +export const retrieveRequestId = (req: Request): string | undefined => { + return req.header('x-request-id'); +}; + +/** + * リクエストのIPアドレスを取得します + * @param {Request} + * @return {string | undefined} + */ +export const retrieveIp = (req: Request): string | undefined => { + // ローカル環境では直近の送信元IPを取得する + if (process.env.STAGE === 'local') { + return req.ip; + } + const ip = req.header('x-forwarded-for'); + if (typeof ip === 'string') { + return ip; + } + return undefined; +}; diff --git a/data_migration_tools/server/src/common/log/index.ts b/data_migration_tools/server/src/common/log/index.ts new file mode 100644 index 0000000..386f9cd --- /dev/null +++ b/data_migration_tools/server/src/common/log/index.ts @@ -0,0 +1,4 @@ +import { Context } from "./types"; +import { makeContext, retrieveRequestId, retrieveIp } from "./context"; + +export { Context, makeContext, retrieveRequestId, retrieveIp }; diff --git a/data_migration_tools/server/src/common/log/types.ts b/data_migration_tools/server/src/common/log/types.ts new file mode 100644 index 0000000..6f56bc1 --- /dev/null +++ b/data_migration_tools/server/src/common/log/types.ts @@ -0,0 +1,34 @@ +export class Context { + /** + * APIの操作ユーザーを追跡するためのID + */ + trackingId: string; + /** + * APIの操作ユーザーのIPアドレス + */ + ip: string; + /** + * ユーザーの操作を一意に識別するためのID + */ + requestId: string; + /** + * APIの代行操作ユーザーを追跡するためのID + */ + delegationId?: string | undefined; + + constructor(externalId: string, requestId: string, delegationId?: string) { + this.trackingId = externalId; + this.delegationId = delegationId; + this.requestId = requestId; + } + /** + * ログにユーザーを特定する情報を出力する + */ + getTrackingId(): string { + if (this.delegationId) { + return `${this.requestId}_${this.trackingId} by ${this.delegationId}`; + } else { + return `${this.requestId}_${this.trackingId}`; + } + } +} diff --git a/data_migration_tools/server/src/common/password/index.ts b/data_migration_tools/server/src/common/password/index.ts new file mode 100644 index 0000000..1ba44a2 --- /dev/null +++ b/data_migration_tools/server/src/common/password/index.ts @@ -0,0 +1,3 @@ +import { makePassword } from "./password"; + +export { makePassword }; diff --git a/data_migration_tools/server/src/common/password/password.ts b/data_migration_tools/server/src/common/password/password.ts new file mode 100644 index 0000000..6fbe071 --- /dev/null +++ b/data_migration_tools/server/src/common/password/password.ts @@ -0,0 +1,35 @@ +export const makePassword = (): string => { + // パスワードの文字数を決定 + const passLength = 8; + + // パスワードに使用可能な文字を決定(今回はアルファベットの大文字と小文字 + 数字 + symbolsの記号) + const lowerCase = "abcdefghijklmnopqrstuvwxyz"; + const upperCase = lowerCase.toLocaleUpperCase(); + const numbers = "0123456789"; + const symbols = "@#$%^&*\\-_+=[]{}|:',.?/`~\"();!"; + const chars = lowerCase + upperCase + numbers + symbols; + + // 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ + const charaTypePattern = + /^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/; + + // autoGeneratedPasswordが以上の条件を満たせばvalidがtrueになる + let valid = false; + let autoGeneratedPassword: string = ""; + + while (!valid) { + // パスワードをランダムに決定 + while (autoGeneratedPassword.length < passLength) { + // 上で決定したcharsの中からランダムに1文字ずつ追加 + const index = Math.floor(Math.random() * chars.length); + autoGeneratedPassword += chars[index]; + } + + // パスワードが上で決定した条件をすべて満たしているかチェック + // 条件を満たすまでループ + valid = + autoGeneratedPassword.length == passLength && + charaTypePattern.test(autoGeneratedPassword); + } + return autoGeneratedPassword; +}; diff --git a/data_migration_tools/server/src/common/repository/index.ts b/data_migration_tools/server/src/common/repository/index.ts new file mode 100644 index 0000000..b3e21fa --- /dev/null +++ b/data_migration_tools/server/src/common/repository/index.ts @@ -0,0 +1,143 @@ +import { + ObjectLiteral, + Repository, + EntityTarget, + UpdateResult, + DeleteResult, + UpdateQueryBuilder, + Brackets, + FindOptionsWhere, +} from 'typeorm'; +import { Context } from '../log'; + +/** + * VS Code上で型解析エラーが発生するため、typeorm内の型定義と同一の型定義をここに記述する + */ +type QueryDeepPartialEntity = _QueryDeepPartialEntity< + ObjectLiteral extends T ? unknown : T +>; +type _QueryDeepPartialEntity = { + [P in keyof T]?: + | (T[P] extends Array + ? Array<_QueryDeepPartialEntity> + : T[P] extends ReadonlyArray + ? ReadonlyArray<_QueryDeepPartialEntity> + : _QueryDeepPartialEntity) + | (() => string); +}; + +interface InsertEntityOptions { + id: number; +} + +const insertEntity = async ( + entity: EntityTarget, + repository: Repository, + value: QueryDeepPartialEntity, + isCommentOut: boolean, + context: Context, +): Promise => { + let query = repository.createQueryBuilder().insert().into(entity); + if (isCommentOut) { + query = query.comment( + `${context.getTrackingId()}_${new Date().toUTCString()}`, + ); + } + const result = await query.values(value).execute(); + // result.identifiers[0].idがnumber型でない場合はエラー + if (typeof result.identifiers[0].id !== 'number') { + throw new Error('Failed to insert entity'); + } + const where: FindOptionsWhere = { id: result.identifiers[0].id } as T; + + // 結果をもとにセレクトする + const inserted = await repository.findOne({ + where, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!inserted) { + throw new Error('Failed to insert entity'); + } + return inserted; +}; + +const insertEntities = async ( + entity: EntityTarget, + repository: Repository, + values: QueryDeepPartialEntity[], + isCommentOut: boolean, + context: Context, +): Promise => { + let query = repository.createQueryBuilder().insert().into(entity); + if (isCommentOut) { + query = query.comment( + `${context.getTrackingId()}_${new Date().toUTCString()}`, + ); + } + const result = await query.values(values).execute(); + + // 挿入するレコードが0で、結果も0であれば、からの配列を返す + if (values.length === 0 && result.identifiers.length === 0) { + return []; + } + + // 挿入するレコード数と挿入されたレコード数が一致しない場合はエラー + if (result.identifiers.length !== values.length) { + throw new Error('Failed to insert entities'); + } + const where: FindOptionsWhere[] = result.identifiers.map((i) => { + // idがnumber型でない場合はエラー + if (typeof i.id !== 'number') { + throw new Error('Failed to insert entities'); + } + return { id: i.id } as T; + }); + + // 結果をもとにセレクトする + const inserted = await repository.find({ + where, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!inserted) { + throw new Error('Failed to insert entity'); + } + return inserted; +}; + +const updateEntity = async ( + repository: Repository, + criteria: + | string + | ((qb: UpdateQueryBuilder) => string) + | Brackets + | ObjectLiteral + | ObjectLiteral[], + values: QueryDeepPartialEntity, + isCommentOut: boolean, + context: Context, +): Promise => { + let query = repository.createQueryBuilder().update(); + if (isCommentOut) { + query = query.comment( + `${context.getTrackingId()}_${new Date().toUTCString()}`, + ); + } + return await query.set(values).where(criteria).execute(); +}; + +const deleteEntity = async ( + repository: Repository, + criteria: string | Brackets | ObjectLiteral | ObjectLiteral[], + isCommentOut: boolean, + context: Context, +): Promise => { + let query = repository.createQueryBuilder().delete(); + if (isCommentOut) { + query = query.comment( + `${context.getTrackingId()}_${new Date().toUTCString()}`, + ); + } + return await query.where(criteria).execute(); +}; + +export { insertEntity, insertEntities, updateEntity, deleteEntity }; diff --git a/data_migration_tools/server/src/common/types/role/index.ts b/data_migration_tools/server/src/common/types/role/index.ts new file mode 100644 index 0000000..d29201f --- /dev/null +++ b/data_migration_tools/server/src/common/types/role/index.ts @@ -0,0 +1,10 @@ +import { ADMIN_ROLES, USER_ROLES } from '../../../constants'; + +/** + * Token.roleに配置されうる文字列リテラル型 + */ +export type Roles = + | (typeof ADMIN_ROLES)[keyof typeof ADMIN_ROLES] + | (typeof USER_ROLES)[keyof typeof USER_ROLES]; + +export type UserRoles = (typeof USER_ROLES)[keyof typeof USER_ROLES]; diff --git a/data_migration_tools/server/src/common/types/sort/index.ts b/data_migration_tools/server/src/common/types/sort/index.ts new file mode 100644 index 0000000..b505e50 --- /dev/null +++ b/data_migration_tools/server/src/common/types/sort/index.ts @@ -0,0 +1,27 @@ +import { + TASK_LIST_SORTABLE_ATTRIBUTES, + SORT_DIRECTIONS, +} from '../../../constants'; + +export type TaskListSortableAttribute = + (typeof TASK_LIST_SORTABLE_ATTRIBUTES)[number]; + +export type SortDirection = (typeof SORT_DIRECTIONS)[number]; + +export const isTaskListSortableAttribute = ( + arg: string, +): arg is TaskListSortableAttribute => { + const param = arg as TaskListSortableAttribute; + if (TASK_LIST_SORTABLE_ATTRIBUTES.includes(param)) { + return true; + } + return false; +}; + +export const isSortDirection = (arg: string): arg is SortDirection => { + const param = arg as SortDirection; + if (SORT_DIRECTIONS.includes(param)) { + return true; + } + return false; +}; diff --git a/data_migration_tools/server/src/common/types/sort/util.ts b/data_migration_tools/server/src/common/types/sort/util.ts new file mode 100644 index 0000000..2ccf33d --- /dev/null +++ b/data_migration_tools/server/src/common/types/sort/util.ts @@ -0,0 +1,11 @@ +import { SortDirection, TaskListSortableAttribute } from '.'; + +export const getDirection = (direction: SortDirection): SortDirection => { + return direction; +}; + +export const getTaskListSortableAttribute = ( + TaskListSortableAttribute: TaskListSortableAttribute, +): TaskListSortableAttribute => { + return TaskListSortableAttribute; +}; diff --git a/data_migration_tools/server/src/common/types/types.ts b/data_migration_tools/server/src/common/types/types.ts new file mode 100644 index 0000000..660b942 --- /dev/null +++ b/data_migration_tools/server/src/common/types/types.ts @@ -0,0 +1,231 @@ +export class csvInputFile { + type: string; + account_id: string; + parent_id: string; + email: string; + company_name: string; + first_name: string; + last_name: string; + country: string; + state: string; + start_date: Date; + expired_date: Date; + user_email: string; + author_id: string; + recording_mode: string; + wt1: string; + wt2: string; + wt3: string; + wt4: string; + wt5: string; + wt6: string; + wt7: string; + wt8: string; + wt9: string; + wt10: string; + wt11: string; + wt12: string; + wt13: string; + wt14: string; + wt15: string; + wt16: string; + wt17: string; + wt18: string; + wt19: string; + wt20: string; +} +export class AccountsOutputFileStep1 { + accountId: number; + type: string; + companyName: string; + country: string; + dealerAccountId?: number; + adminName: string; + adminMail: string; + userId: number; +} + +export class AccountsOutputFile { + accountId: number; + type: number; + companyName: string; + country: string; + dealerAccountId?: number; + adminName: string; + adminMail: string; + userId: number; +} +export class AccountsInputFile { + accountId: number; + type: number; + companyName: string; + country: string; + dealerAccountId?: number; + adminName: string; + adminMail: string; + userId: number; +} +export class UsersOutputFile { + accountId: number; + userId: number; + name: string; + role: string; + authorId: string; + email: string; +} + +export class UsersInputFile { + accountId: number; + userId: number; + name: string; + role: string; + authorId: string; + email: string; +} + +export class LicensesOutputFile { + expiry_date: string; + account_id: number; + type: string; + status: string; + allocated_user_id?: number; +} +export class LicensesInputFile { + expiry_date: string; + account_id: number; + type: string; + status: string; + allocated_user_id?: number; +} + +export class WorktypesOutputFile { + account_id: number; + custom_worktype_id: string; +} +export class WorktypesInputFile { + account_id: number; + custom_worktype_id: string; +} + +export class CardLicensesInputFile { + license_id: number; + issue_id: number; + card_license_key: string; + activated_at?: string; + created_at?: string; + created_by?: string; + updated_at?: string; + updated_by?: string; +} + +export function isAccountsInputFileArray(obj: any): obj is AccountsInputFile[] { + return Array.isArray(obj) && obj.every((item) => isAccountsInputFile(item)); +} +export function isAccountsInputFile(obj: any): obj is AccountsInputFile { + return ( + typeof obj === "object" && + obj !== null && + "accountId" in obj && + typeof obj.accountId === "number" && + "type" in obj && + typeof obj.type === "number" && + "companyName" in obj && + typeof obj.companyName === "string" && + "country" in obj && + typeof obj.country === "string" && + ("dealerAccountId" in obj + ? typeof obj.dealerAccountId === "number" + : true) && + "adminName" in obj && + typeof obj.adminName === "string" && + "adminMail" in obj && + typeof obj.adminMail === "string" && + "userId" in obj && + typeof obj.userId === "number" + ); +} + +export function isUsersInputFileArray(obj: any): obj is UsersInputFile[] { + return Array.isArray(obj) && obj.every((item) => isUsersInputFile(item)); +} +export function isUsersInputFile(obj: any): obj is UsersInputFile { + return ( + typeof obj === "object" && + obj !== null && + "accountId" in obj && + "userId" in obj && + "name" in obj && + "role" in obj && + "authorId" in obj && + "email" in obj && + typeof obj.accountId === "number" && + typeof obj.userId === "number" && + typeof obj.name === "string" && + typeof obj.role === "string" && + typeof obj.authorId === "string" && + typeof obj.email === "string" + ); +} + +export function isLicensesInputFileArray(obj: any): obj is LicensesInputFile[] { + return Array.isArray(obj) && obj.every((item) => isLicensesInputFile(item)); +} +export function isLicensesInputFile(obj: any): obj is LicensesInputFile { + return ( + typeof obj === "object" && + obj !== null && + "expiry_date" in obj && + "account_id" in obj && + "type" in obj && + "status" in obj && + typeof obj.expiry_date === "string" && + typeof obj.account_id === "number" && + typeof obj.type === "string" && + typeof obj.status === "string" && + (obj.allocated_user_id === null || + typeof obj.allocated_user_id === "number") + ); +} + +export function isWorktypesInputFileArray( + obj: any +): obj is WorktypesInputFile[] { + return Array.isArray(obj) && obj.every((item) => isWorktypesInputFile(item)); +} +export function isWorktypesInputFile(obj: any): obj is WorktypesInputFile { + return ( + typeof obj === "object" && + obj !== null && + "account_id" in obj && + "custom_worktype_id" in obj && + typeof obj.account_id === "number" && + typeof obj.custom_worktype_id === "string" + ); +} + +export function isCardLicensesInputFileArray( + obj: any +): obj is CardLicensesInputFile[] { + return ( + Array.isArray(obj) && obj.every((item) => isCardLicensesInputFile(item)) + ); +} +export function isCardLicensesInputFile( + obj: any +): obj is CardLicensesInputFile { + return ( + typeof obj === "object" && + obj !== null && + "license_id" in obj && + "issue_id" in obj && + "card_license_key" in obj && + typeof obj.license_id === "number" && + typeof obj.issue_id === "number" && + typeof obj.card_license_key === "string" && + (obj.activated_at === null || typeof obj.activated_at === "string") && + (obj.created_at === null || typeof obj.created_at === "string") && + (obj.created_by === null || typeof obj.created_by === "string") && + (obj.updated_at === null || typeof obj.updated_at === "string") && + (obj.updated_by === null || typeof obj.updated_by === "string") + ); +} diff --git a/data_migration_tools/server/src/common/validators/admin.validator.ts b/data_migration_tools/server/src/common/validators/admin.validator.ts new file mode 100644 index 0000000..d904154 --- /dev/null +++ b/data_migration_tools/server/src/common/validators/admin.validator.ts @@ -0,0 +1,39 @@ +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from "class-validator"; + +@ValidatorConstraint() +export class IsAdminPassword implements ValidatorConstraintInterface { + validate(value: string): boolean { + // 8文字~64文字でなければ早期に不合格 + const minLength = 8; + const maxLength = 64; + if (value.length < minLength || value.length > maxLength) { + return false; + } + + // 英字の大文字、英字の小文字、アラビア数字、記号(@#$%^&*\-_+=[]{}|\:',.?/`~"();!)から2種類以上組み合わせ + const charaTypePattern = + /^((?=.*[a-z])(?=.*[A-Z])|(?=.*[a-z])(?=.*[\d])|(?=.*[a-z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[A-Z])(?=.*[\d])|(?=.*[A-Z])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!])|(?=.*[\d])(?=.*[@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]))[a-zA-Z\d@#$%^&*\\\-_+=\[\]{}|:',.?\/`~"();!]/; + return new RegExp(charaTypePattern).test(value); + } + defaultMessage(): string { + return "Admin password rule not satisfied"; + } +} + +export const IsAdminPasswordvalid = (validationOptions?: ValidationOptions) => { + return (object: any, propertyName: string) => { + registerDecorator({ + name: "IsAdminPasswordvalid", + target: object.constructor, + propertyName: propertyName, + constraints: [], + options: validationOptions, + validator: IsAdminPassword, + }); + }; +}; diff --git a/data_migration_tools/server/src/constants/index.ts b/data_migration_tools/server/src/constants/index.ts index e363afc..71bd022 100644 --- a/data_migration_tools/server/src/constants/index.ts +++ b/data_migration_tools/server/src/constants/index.ts @@ -1,3 +1,20 @@ +/** + * 階層 + * @const {number} + */ +export const TIERS = { + //OMDS東京 + TIER1: 1, + //OMDS現地法人 + TIER2: 2, + //代理店 + TIER3: 3, + //販売店 + TIER4: 4, + //エンドユーザー + TIER5: 5, +} as const; + /** * 音声ファイルをEast USに保存する国リスト * @const {number} @@ -52,6 +69,213 @@ export const BLOB_STORAGE_REGION_EU = [ "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 USER_ROLE_ORDERS = [ + USER_ROLES.AUTHOR, + USER_ROLES.TYPIST, + USER_ROLES.NONE, +] as string[]; + +/** + * ライセンス注文状態 + * @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; + +/** + * タイムゾーンを加味したライセンスの有効期間(8時間) + * @const {number} + */ +export const LICENSE_EXPIRATION_TIME_WITH_TIMEZONE = 8; + +/** + * カードライセンスの桁数 + * @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_EXPIRY_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; + +/** + * オプションアイテムのタイプ文字列と数値の対応 + **/ +export const OPTION_ITEM_VALUE_TYPE_NUMBER: { + type: string; + value: number; +}[] = [ + { + type: OPTION_ITEM_VALUE_TYPE.BLANK, + value: 1, + }, + { + type: OPTION_ITEM_VALUE_TYPE.DEFAULT, + value: 2, + }, + { + type: OPTION_ITEM_VALUE_TYPE.LAST_INPUT, + value: 3, + }, +]; + /** * ADB2Cユーザのidentity.signInType * @const {string[]} @@ -60,8 +284,123 @@ 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", + PRIVACY_NOTICE: "PrivacyNotice", +} as const; + +/** + * 音声ファイルのフォーマット + * @const {string} + */ +export const USER_AUDIO_FORMAT = "DS2(QP)"; + +/** + * ユニットテスト実行をしている場合のNODE_ENVの値 + * @const {string[]} + */ +export const NODE_ENV_TEST = "test"; + +/** + * ユーザに対するライセンスの状態 + * @const {string[]} + */ +export const USER_LICENSE_STATUS = { + UNALLOCATED: "unallocated", + ALLOCATED: "allocated", + EXPIRED: "expired", +} as const; + +/** + * typeの取りうる値(移行元CSV) + * @const {string[]} + */ +export const MIGRATION_TYPE = { + ADMINISTRATOR: "Administrator", + BC: "BC", + COUNTRY: "Country", + CUSTOMER: "Customer", + DEALER: "Dealer", + DISTRIBUTOR: "Distributor", + USER: "USER", +} as const; + +/** + * 移行先の名称と移行元の値 + * @const {string[]} + */ +export const COUNTRY_LIST = [ + { value: "CA", label: "Canada" }, + { value: "KY", label: "Cayman Islands" }, + { value: "US", label: "United States" }, + { value: "AU", label: "Australia" }, + { value: "NZ", label: "New Zealand" }, + { value: "AT", label: "Austria" }, + { value: "BE", label: "Belgium" }, + { value: "BG", label: "Bulgaria" }, + { value: "HR", label: "Croatia" }, + { value: "CY", label: "Cyprus" }, + { value: "CZ", label: "Czech Republic" }, + { value: "DK", label: "Denmark" }, + { value: "EE", label: "Estonia" }, + { value: "FI", label: "Finland" }, + { value: "FR", label: "France" }, + { value: "DE", label: "Germany" }, + { value: "GR", label: "Greece" }, + { value: "HU", label: "Hungary" }, + { value: "IS", label: "Iceland" }, + { value: "IE", label: "Ireland" }, + { value: "IT", label: "Italy" }, + { value: "LV", label: "Latvia" }, + { value: "LI", label: "Liechtenstein" }, + { value: "LT", label: "Lithuania" }, + { value: "LU", label: "Luxembourg" }, + { value: "MT", label: "Malta" }, + { value: "NL", label: "Netherlands" }, + { value: "NO", label: "Norway" }, + { value: "PL", label: "Poland" }, + { value: "PT", label: "Portugal" }, + { value: "RO", label: "Romania" }, + { value: "RS", label: "Serbia" }, + { value: "SK", label: "Slovakia" }, + { value: "SI", label: "Slovenia" }, + { value: "ZA", label: "South Africa" }, + { value: "ES", label: "Spain" }, + { value: "SE", label: "Sweden" }, + { value: "CH", label: "Switzerland" }, + { value: "TR", label: "Turkey" }, + { value: "GB", label: "United Kingdom" }, +]; + +/** + * recording_modeの取りうる値(移行元CSV) + * @const {string[]} + */ +export const RECORDING_MODE = { + DS2_QP: "DS2 (QP)", + DS2_SP: "DS2 (SP)", + DSS: "DSS", +} as const; + /** * AutoIncrementの初期値 * @const {number} */ export const AUTO_INCREMENT_START = 853211; + +/** + * 移行データ登録時のsleep間隔 + * @const {number} + */ +export const MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC = 13; diff --git a/data_migration_tools/server/src/features/accounts/accounts.controller.ts b/data_migration_tools/server/src/features/accounts/accounts.controller.ts new file mode 100644 index 0000000..97ae0b9 --- /dev/null +++ b/data_migration_tools/server/src/features/accounts/accounts.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Logger } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { AccountsService } from "./accounts.service"; + +@ApiTags("accounts") +@Controller("accounts") +export class AccountsController { + private readonly logger = new Logger(AccountsController.name); + constructor( + private readonly accountService: AccountsService //private readonly cryptoService: CryptoService, + ) {} +} diff --git a/data_migration_tools/server/src/features/accounts/accounts.module.ts b/data_migration_tools/server/src/features/accounts/accounts.module.ts new file mode 100644 index 0000000..cb6db2b --- /dev/null +++ b/data_migration_tools/server/src/features/accounts/accounts.module.ts @@ -0,0 +1,19 @@ +import { Module } from "@nestjs/common"; +import { UsersRepositoryModule } from "../../repositories/users/users.repository.module"; +import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module"; +import { AccountsController } from "./accounts.controller"; +import { AccountsService } from "./accounts.service"; +import { AdB2cModule } from "../../gateways/adb2c/adb2c.module"; +import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module"; + +@Module({ + imports: [ + AccountsRepositoryModule, + UsersRepositoryModule, + AdB2cModule, + BlobstorageModule, + ], + controllers: [AccountsController], + providers: [AccountsService], +}) +export class AccountsModule {} diff --git a/data_migration_tools/server/src/features/accounts/accounts.service.ts b/data_migration_tools/server/src/features/accounts/accounts.service.ts new file mode 100644 index 0000000..4a7dd65 --- /dev/null +++ b/data_migration_tools/server/src/features/accounts/accounts.service.ts @@ -0,0 +1,227 @@ +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { AccountsRepositoryService } from "../../repositories/accounts/accounts.repository.service"; +import { + AdB2cService, + ConflictError, + isConflictError, +} from "../../gateways/adb2c/adb2c.service"; +import { Account } from "../../repositories/accounts/entity/account.entity"; +import { User } from "../../repositories/users/entity/user.entity"; +import { MANUAL_RECOVERY_REQUIRED } from "../../constants"; +import { makeErrorResponse } from "../../common/error/makeErrorResponse"; +import { Context } from "../../common/log"; +import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service"; + +@Injectable() +export class AccountsService { + constructor( + private readonly accountRepository: AccountsRepositoryService, + private readonly adB2cService: AdB2cService, + private readonly blobStorageService: BlobstorageService + ) {} + private readonly logger = new Logger(AccountsService.name); + + /** + * アカウント情報をDBに作成する + * @param companyName + * @param country + * @param [dealerAccountId] + * @returns account + */ + async createAccount( + context: Context, + companyName: string, + country: string, + dealerAccountId: number | undefined, + email: string, + password: string, + username: string, + role: string, + acceptedEulaVersion: string, + acceptedPrivacyNoticeVersion: string, + acceptedDpaVersion: string, + type: number, + accountId: number, + userId: number + ): Promise<{ accountId: number; userId: number; externalUserId: string }> { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.createAccount.name + } | params: { ` + + `dealerAccountId: ${dealerAccountId}, ` + + `role: ${role}, ` + + `acceptedEulaVersion: ${acceptedEulaVersion}, ` + + `acceptedPrivacyNoticeVersion: ${acceptedPrivacyNoticeVersion}, ` + + `acceptedDpaVersion: ${acceptedDpaVersion}, ` + + `type: ${type}, ` + + `accountId: ${accountId}, ` + + `userId: ${userId} };` + ); + try { + let externalUser: { sub: string } | ConflictError; + try { + // idpにユーザーを作成 + externalUser = await this.adB2cService.createUser( + context, + email, + password, + username + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error( + `[${context.getTrackingId()}] create externalUser failed` + ); + + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + // メールアドレス重複エラー + if (isConflictError(externalUser)) { + this.logger.error( + `[${context.getTrackingId()}] email conflict. externalUser: ${externalUser}` + ); + throw new HttpException( + makeErrorResponse("E010301"), + HttpStatus.BAD_REQUEST + ); + } + + let account: Account; + let user: User; + try { + // アカウントと管理者をセットで作成 + const { newAccount, adminUser } = + await this.accountRepository.createAccount( + context, + companyName, + country, + dealerAccountId, + type, + externalUser.sub, + role, + accountId, + userId, + acceptedEulaVersion, + acceptedPrivacyNoticeVersion, + acceptedDpaVersion + ); + account = newAccount; + user = adminUser; + this.logger.log( + `[${context.getTrackingId()}] adminUser.external_id: ${ + user.external_id + }` + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error(`[${context.getTrackingId()}] create account failed`); + //リカバリ処理 + // idpのユーザーを削除 + await this.deleteAdB2cUser(externalUser.sub, context); + + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + // 新規作成アカウント用のBlobコンテナを作成 + try { + await this.blobStorageService.createContainer( + context, + account.id, + country + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error( + `[${context.getTrackingId()}] create container failed` + ); + //リカバリ処理 + // idpのユーザーを削除 + await this.deleteAdB2cUser(externalUser.sub, context); + + // DBのアカウントを削除 + await this.deleteAccount(account.id, user.id, context); + + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + return { + accountId: account.id, + userId: user.id, + externalUserId: user.external_id, + }; + } catch (e) { + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createAccount.name}` + ); + } + } + + // AdB2cのユーザーを削除 + // TODO「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 + private async deleteAdB2cUser( + externalUserId: string, + context: Context + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.createAccount.name + } | params: { ` + `externalUserId: ${externalUserId}};` + ); + try { + await this.adB2cService.deleteUser(externalUserId, context); + this.logger.log( + `[${context.getTrackingId()}] delete externalUser: ${externalUserId} | params: { ` + + `externalUserId: ${externalUserId}, };` + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}` + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteAdB2cUser.name}` + ); + } + } + + // DBのアカウントを削除 + private async deleteAccount( + accountId: number, + userId: number, + context: Context + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteAccount.name + } | params: { accountId: ${accountId}, userId: ${userId} };` + ); + try { + await this.accountRepository.deleteAccount(context, accountId, userId); + this.logger.log( + `[${context.getTrackingId()}] delete account: ${accountId}, user: ${userId}` + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete account: ${accountId}, user: ${userId}` + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteAccount.name}` + ); + } + } +} diff --git a/data_migration_tools/server/src/features/delete/delete.controller.ts b/data_migration_tools/server/src/features/delete/delete.controller.ts index 94567ca..ed81c5a 100644 --- a/data_migration_tools/server/src/features/delete/delete.controller.ts +++ b/data_migration_tools/server/src/features/delete/delete.controller.ts @@ -11,6 +11,7 @@ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { Request } from "express"; import { DeleteService } from "./delete.service"; import { DeleteResponse } from "./types/types"; +import { makeContext } from "src/common/log"; @ApiTags("delete") @Controller("delete") @@ -33,7 +34,9 @@ export class DeleteController { }) @Post() async deleteData(): Promise<{}> { - await this.deleteService.deleteData(); + const context = makeContext("tool", "delete"); + + await this.deleteService.deleteData(context); return {}; } } diff --git a/data_migration_tools/server/src/features/delete/delete.service.ts b/data_migration_tools/server/src/features/delete/delete.service.ts index 9ade573..3644220 100644 --- a/data_migration_tools/server/src/features/delete/delete.service.ts +++ b/data_migration_tools/server/src/features/delete/delete.service.ts @@ -3,6 +3,7 @@ import { DeleteRepositoryService } from "../../repositories/delete/delete.reposi import { makeErrorResponse } from "../../common/errors/makeErrorResponse"; import { AdB2cService } from "../../gateways/adb2c/adb2c.service"; import { BlobstorageService } from "../../gateways/blobstorage/blobstorage.service"; +import { Context } from "../../common/log"; @Injectable() export class DeleteService { @@ -11,27 +12,41 @@ export class DeleteService { private readonly deleteRepositoryService: DeleteRepositoryService, private readonly blobstorageService: BlobstorageService, private readonly adB2cService: AdB2cService - ) {} + ) { } /** * データを削除する * @returns data */ - async deleteData(): Promise { - this.logger.log(`[IN] ${this.deleteData.name}`); + async deleteData(context: Context): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.deleteData.name}` + ); try { // BlobStorageからデータを削除する - await this.blobstorageService.deleteContainers(); + await this.blobstorageService.deleteContainers(context); - // ADB2Cからユーザ情報を取得する - const users = await this.adB2cService.getUsers(); - const externalIds = users.map((user) => user.id); - await this.adB2cService.deleteUsers(externalIds); + // 100件ずつのユーザー取得なのですべて削除するまでループする + for (let i = 0; i < 500; i++) { + // ADB2Cからユーザ情報を取得する + const { users, hasNext } = await this.adB2cService.getUsers(context); + + + const externalIds = users.map((user) => user.id); + await this.adB2cService.deleteUsers(context, externalIds); + + // 削除していないユーザーがいない場合はループを抜ける + if (!hasNext) { + break; + } + } // データベースからデータを削除する await this.deleteRepositoryService.deleteData(); // AutoIncrementの値をリセットする await this.deleteRepositoryService.resetAutoIncrement(); + // 初期データを挿入する + await this.deleteRepositoryService.insertInitData(context); } catch (e) { this.logger.error(`error=${e}`); if (e instanceof Error) { diff --git a/data_migration_tools/server/src/features/register/register.controller.ts b/data_migration_tools/server/src/features/register/register.controller.ts new file mode 100644 index 0000000..c55d548 --- /dev/null +++ b/data_migration_tools/server/src/features/register/register.controller.ts @@ -0,0 +1,209 @@ +import { + Body, + Controller, + HttpStatus, + Post, + Req, + Logger, + HttpException, +} from "@nestjs/common"; +import { makeErrorResponse } from "../../common/error/makeErrorResponse"; +import fs from "fs"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { Request } from "express"; +import { RegisterRequest, RegisterResponse } from "./types/types"; +import { RegisterService } from "./register.service"; +import { AccountsService } from "../accounts/accounts.service"; +import { UsersService } from "../users/users.service"; +import { makeContext } from "../../common/log"; +import { + isAccountsInputFileArray, + isUsersInputFileArray, + isLicensesInputFileArray, + isWorktypesInputFileArray, + isCardLicensesInputFileArray, +} from "../../common/types/types"; +import { makePassword } from "../../common/password/password"; +import { + USER_ROLES, + MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC, +} from "../../constants"; + +@ApiTags("register") +@Controller("register") +export class RegisterController { + private readonly logger = new Logger(RegisterController.name); + constructor( + private readonly registerService: RegisterService, + private readonly accountsService: AccountsService, + private readonly usersService: UsersService + ) {} + + @Post() + @ApiResponse({ + status: HttpStatus.OK, + type: RegisterResponse, + description: "成功時のレスポンス", + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: "想定外のサーバーエラー", + }) + @ApiOperation({ operationId: "dataRegist" }) + async dataRegist( + @Body() body: RegisterRequest, + @Req() req: Request + ): Promise { + const context = makeContext("iko", "register"); + + const inputFilePath = body.inputFilePath; + + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.dataRegist.name + } | params: { inputFilePath: ${inputFilePath}};` + ); + + try { + // 読み込みファイルのフルパス + const accouncsFileFullPath = inputFilePath + "accounts.json"; + const usersFileFullPath = inputFilePath + "users.json"; + const licensesFileFullPath = inputFilePath + "licenses.json"; + const worktypesFileFullPath = inputFilePath + "worktypes.json"; + const cardLicensesFileFullPath = inputFilePath + "cardLicenses.json"; + + // ファイル存在チェックと読み込み + if ( + !fs.existsSync(accouncsFileFullPath) || + !fs.existsSync(usersFileFullPath) || + !fs.existsSync(licensesFileFullPath) || + !fs.existsSync(worktypesFileFullPath) || + !fs.existsSync(cardLicensesFileFullPath) + ) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + + // アカウントの登録用ファイル読み込み + const accountsObject = JSON.parse( + fs.readFileSync(accouncsFileFullPath, "utf8") + ); + + // 型ガード(account) + if (!isAccountsInputFileArray(accountsObject)) { + throw new Error("input file is not accountsInputFiles"); + } + + for (const accountsInputFile of accountsObject) { + // ランダムなパスワードを生成する + const ramdomPassword = makePassword(); + await this.accountsService.createAccount( + context, + accountsInputFile.companyName, + accountsInputFile.country, + accountsInputFile.dealerAccountId, + accountsInputFile.adminMail, + ramdomPassword, + accountsInputFile.adminName, + "none", + null, + null, + null, + accountsInputFile.type, + accountsInputFile.accountId, + accountsInputFile.userId + ); + + // ratelimit対応のためsleepを行う + await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC); + } + // const accountsInputFiles = accountsObject as AccountsInputFile[]; + + // ユーザの登録用ファイル読み込み + const usersObject = JSON.parse( + fs.readFileSync(usersFileFullPath, "utf8") + ); + + // 型ガード(user) + if (!isUsersInputFileArray(usersObject)) { + throw new Error("input file is not usersInputFiles"); + } + + for (const usersInputFile of usersObject) { + this.logger.log(usersInputFile.name); + await this.usersService.createUser( + context, + usersInputFile.name, + usersInputFile.role === USER_ROLES.AUTHOR + ? USER_ROLES.AUTHOR + : USER_ROLES.NONE, + usersInputFile.email, + true, + true, + usersInputFile.accountId, + usersInputFile.userId, + usersInputFile.authorId, + false, + null, + true + ); + // ratelimit対応のためsleepを行う + await sleep(MIGRATION_DATA_REGISTER_INTERVAL_MILLISEC); + } + + // ライセンスの登録用ファイル読み込み + const licensesObject = JSON.parse( + fs.readFileSync(licensesFileFullPath, "utf8") + ); + + // 型ガード(license) + if (!isLicensesInputFileArray(licensesObject)) { + throw new Error("input file is not licensesInputFiles"); + } + + // ワークタイプの登録用ファイル読み込み + const worktypesObject = JSON.parse( + fs.readFileSync(worktypesFileFullPath, "utf8") + ); + + // 型ガード(Worktypes) + if (!isWorktypesInputFileArray(worktypesObject)) { + throw new Error("input file is not WorktypesInputFiles"); + } + + // カードライセンスの登録用ファイル読み込み + const cardLicensesObject = JSON.parse( + fs.readFileSync(cardLicensesFileFullPath, "utf8") + ); + + // 型ガード(cardLicenses) + if (!isCardLicensesInputFileArray(cardLicensesObject)) { + throw new Error("input file is not cardLicensesInputFiles"); + } + + // ライセンス・ワークタイプ・カードライセンスの登録 + await this.registerService.registLicenseAndWorktypeData( + context, + licensesObject, + worktypesObject, + cardLicensesObject + ); + + return {}; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}` + ); + } + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/data_migration_tools/server/src/features/register/register.module.ts b/data_migration_tools/server/src/features/register/register.module.ts new file mode 100644 index 0000000..10015dd --- /dev/null +++ b/data_migration_tools/server/src/features/register/register.module.ts @@ -0,0 +1,25 @@ +import { Module } from "@nestjs/common"; +import { RegisterController } from "./register.controller"; +import { RegisterService } from "./register.service"; +import { AccountsService } from "../accounts/accounts.service"; +import { UsersService } from "../users/users.service"; +import { LicensesRepositoryModule } from "../../repositories/licenses/licenses.repository.module"; +import { WorktypesRepositoryModule } from "../../repositories/worktypes/worktypes.repository.module"; +import { UsersRepositoryModule } from "../../repositories/users/users.repository.module"; +import { AccountsRepositoryModule } from "../../repositories/accounts/accounts.repository.module"; +import { AdB2cModule } from "../../gateways/adb2c/adb2c.module"; +import { BlobstorageModule } from "../../gateways/blobstorage/blobstorage.module"; + +@Module({ + imports: [ + LicensesRepositoryModule, + WorktypesRepositoryModule, + AccountsRepositoryModule, + UsersRepositoryModule, + AdB2cModule, + BlobstorageModule, + ], + controllers: [RegisterController], + providers: [RegisterService, AccountsService, UsersService], +}) +export class RegisterModule {} diff --git a/data_migration_tools/server/src/features/register/register.service.ts b/data_migration_tools/server/src/features/register/register.service.ts new file mode 100644 index 0000000..6e42dc1 --- /dev/null +++ b/data_migration_tools/server/src/features/register/register.service.ts @@ -0,0 +1,68 @@ +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { Context } from "../../common/log"; +import { + LicensesInputFile, + WorktypesInputFile, + CardLicensesInputFile, +} from "../../common/types/types"; +import { LicensesRepositoryService } from "../../repositories/licenses/licenses.repository.service"; +import { WorktypesRepositoryService } from "../../repositories/worktypes/worktypes.repository.service"; +import { makeErrorResponse } from "../../common/error/makeErrorResponse"; +@Injectable() +export class RegisterService { + constructor( + private readonly licensesRepository: LicensesRepositoryService, + private readonly worktypesRepository: WorktypesRepositoryService + ) {} + private readonly logger = new Logger(RegisterService.name); + + /** + * Regist Data + * @param inputFilePath: string + */ + async registLicenseAndWorktypeData( + context: Context, + licensesInputFiles: LicensesInputFile[], + worktypesInputFiles: WorktypesInputFile[], + cardlicensesInputFiles: CardLicensesInputFile[] + ): Promise { + // パラメータ内容が長大なのでログには出さない + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.registLicenseAndWorktypeData.name + }` + ); + + try { + this.logger.log("Licenses register start"); + await this.licensesRepository.insertLicenses(context, licensesInputFiles); + this.logger.log("Licenses register end"); + + this.logger.log("Worktypes register start"); + await this.worktypesRepository.createWorktype( + context, + worktypesInputFiles + ); + this.logger.log("Worktypes register end"); + + this.logger.log("CardLicenses register start"); + await this.licensesRepository.insertCardLicenses( + context, + cardlicensesInputFiles + ); + this.logger.log("CardLicenses register end"); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.registLicenseAndWorktypeData.name + }` + ); + } + } +} diff --git a/data_migration_tools/server/src/features/register/types/types.ts b/data_migration_tools/server/src/features/register/types/types.ts new file mode 100644 index 0000000..30fe678 --- /dev/null +++ b/data_migration_tools/server/src/features/register/types/types.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RegisterRequest { + @ApiProperty() + inputFilePath: string; + +} + +export class RegisterResponse {} + diff --git a/data_migration_tools/server/src/features/transfer/transfer.controller.ts b/data_migration_tools/server/src/features/transfer/transfer.controller.ts new file mode 100644 index 0000000..8725a4c --- /dev/null +++ b/data_migration_tools/server/src/features/transfer/transfer.controller.ts @@ -0,0 +1,178 @@ +import { + Body, + Controller, + HttpStatus, + Post, + Req, + HttpException, + Logger, +} from "@nestjs/common"; +import fs from "fs"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { Request } from "express"; +import { transferRequest, transferResponse } from "./types/types"; +import { TransferService } from "./transfer.service"; +import { makeContext } from "../../common/log"; +import { csvInputFile } from "../../common/types/types"; +import { makeErrorResponse } from "src/common/errors/makeErrorResponse"; +import { + COUNTRY_LIST, + MIGRATION_TYPE, + TIERS, + WORKTYPE_MAX_COUNT, + RECORDING_MODE, + LICENSE_ALLOCATED_STATUS, + USER_ROLES, + AUTO_INCREMENT_START, +} from "../../constants"; +@ApiTags("transfer") +@Controller("transfer") +export class TransferController { + private readonly logger = new Logger(TransferController.name); + constructor(private readonly transferService: TransferService) {} + + @Post() + @ApiResponse({ + status: HttpStatus.OK, + type: transferResponse, + description: "成功時のレスポンス", + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: "想定外のサーバーエラー", + }) + @ApiOperation({ operationId: "dataRegist" }) + async dataRegist( + @Body() body: transferRequest, + @Req() req: Request + ): Promise { + const context = makeContext("iko", "transfer"); + + const inputFilePath = body.inputFilePath; + + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.dataRegist.name + } | params: { inputFilePath: ${inputFilePath}};` + ); + try { + // 読み込みファイルのフルパス + const csvFileFullPath = inputFilePath + ".csv"; + + // ファイル存在チェックと読み込み + if (!fs.existsSync(csvFileFullPath)) { + this.logger.error(`file not exists from ${inputFilePath}`); + throw new Error(`file not exists from ${inputFilePath}`); + } + + // CSVファイルを全行読み込む + const inputFile = fs.readFileSync(csvFileFullPath, "utf-8"); + + // レコードごとに分割 + const csvInputFileLines = inputFile.split("\n"); + + // ヘッダー行を削除 + csvInputFileLines.shift(); + + // 項目ごとに切り分ける + let csvInputFile: csvInputFile[] = []; + csvInputFileLines.forEach((line) => { + const data = line.split(","); + // ダブルクォーテーションは削除 + data.forEach((value, index) => { + data[index] = value.replace(/"/g, ""); + }); + csvInputFile.push({ + type: data[0], + account_id: data[1], + parent_id: data[2], + email: data[3], + company_name: data[4], + first_name: data[5], + last_name: data[6], + country: data[7], + state: data[8], + start_date: new Date(data[9]), + expired_date: new Date(data[10]), + user_email: data[11], + author_id: data[12], + recording_mode: data[13], + wt1: data[14], + wt2: data[15], + wt3: data[16], + wt4: data[17], + wt5: data[18], + wt6: data[19], + wt7: data[20], + wt8: data[21], + wt9: data[22], + wt10: data[23], + wt11: data[24], + wt12: data[25], + wt13: data[26], + wt14: data[27], + wt15: data[28], + wt16: data[29], + wt17: data[30], + wt18: data[31], + wt19: data[32], + wt20: data[33], + }); + }); + + // 各データのバリデーションチェック + await this.transferService.validateInputData(context, csvInputFile); + + // account_idを通番に変換し、変換前account_id: 変換後accountId配列を作成する。 + const accountIdList = csvInputFile.map((line) => line.account_id); + const accountIdListSet = new Set(accountIdList); + const accountIdListArray = Array.from(accountIdListSet); + const accountIdMap = new Map(); + accountIdListArray.forEach((accountId, index) => { + accountIdMap.set(accountId, index + AUTO_INCREMENT_START); + }); + // CSVファイルの変換 + const transferResponse = await this.transferService.registInputData( + context, + csvInputFile, + accountIdMap + ); + + // countryを除いた階層の再配置 + const accountsOutputFileStep1Lines = + transferResponse.accountsOutputFileStep1Lines; + const accountsOutputFile = await this.transferService.relocateHierarchy( + context, + accountsOutputFileStep1Lines + ); + // メールアドレスの重複を削除 + // デモライセンスの削除 + // いったんこのままコミットしてテストを実施する + + // transferResponseを4つのJSONファイルの出力する(出力先はinputと同じにする) + const outputFilePath = body.inputFilePath; + const usersOutputFile = transferResponse.usersOutputFileLines; + const licensesOutputFile = transferResponse.licensesOutputFileLines; + const worktypesOutputFile = transferResponse.worktypesOutputFileLines; + this.transferService.outputJsonFile( + context, + outputFilePath, + accountsOutputFile, + usersOutputFile, + licensesOutputFile, + worktypesOutputFile + ); + return {}; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.dataRegist.name}` + ); + } + } +} diff --git a/data_migration_tools/server/src/features/transfer/transfer.module.ts b/data_migration_tools/server/src/features/transfer/transfer.module.ts new file mode 100644 index 0000000..d2d9695 --- /dev/null +++ b/data_migration_tools/server/src/features/transfer/transfer.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { TransferController } from "./transfer.controller"; +import { TransferService } from "./transfer.service"; +@Module({ + imports: [], + controllers: [TransferController], + providers: [TransferService], +}) +export class TransferModule {} diff --git a/data_migration_tools/server/src/features/transfer/transfer.service.ts b/data_migration_tools/server/src/features/transfer/transfer.service.ts new file mode 100644 index 0000000..31b75ed --- /dev/null +++ b/data_migration_tools/server/src/features/transfer/transfer.service.ts @@ -0,0 +1,372 @@ +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { Context } from "../../common/log"; +import { + AccountsOutputFileStep1, + UsersOutputFile, + LicensesOutputFile, + WorktypesOutputFile, + csvInputFile, + AccountsOutputFile, +} from "../../common/types/types"; +import { + COUNTRY_LIST, + MIGRATION_TYPE, + TIERS, + WORKTYPE_MAX_COUNT, + RECORDING_MODE, + LICENSE_ALLOCATED_STATUS, + USER_ROLES, + SWITCH_FROM_TYPE, +} from "src/constants"; +import { registInputDataResponse } from "./types/types"; +import fs from "fs"; + +@Injectable() +export class TransferService { + constructor() {} + private readonly logger = new Logger(TransferService.name); + + /** + * Regist Data + * @param OutputFilePath: string + * @param csvInputFile: csvInputFile[] + */ + async registInputData( + context: Context, + csvInputFile: csvInputFile[], + accountIdMap: Map + ): Promise { + // パラメータ内容が長大なのでログには出さない + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.registInputData.name}` + ); + + try { + let accountsOutputFileStep1Lines: AccountsOutputFileStep1[] = []; + let usersOutputFileLines: UsersOutputFile[] = []; + let licensesOutputFileLines: LicensesOutputFile[] = []; + let worktypesOutputFileLines: WorktypesOutputFile[] = []; + + let userIdIndex = 0; + // csvInputFileを一行読み込みする + csvInputFile.forEach((line) => { + // typeが"USER"以外の場合、アカウントデータの作成を行う + if (line.type !== MIGRATION_TYPE.USER) { + // userIdのインクリメント + userIdIndex = userIdIndex + 1; + // line.countryの値を読み込みCOUNTRY_LISTのlabelからvalueに変換する + const country = COUNTRY_LIST.find( + (country) => country.label === line.country + )?.value; + // adminNameの変換(last_name + " "+ first_name) + const adminName = `${line.last_name} ${line.first_name}`; + + // ランダムパスワードの生成(データ登録ツール側で行うのでやらない) + // common/password/password.tsのmakePasswordを使用 + // const autoGeneratedPassword = makePassword(); + + // parentAccountIdの設定 + // parent_idが存在する場合、accountIdMapを参照し、accountIdに変換する + let parentAccountId: number | null = null; + if (line.parent_id) { + parentAccountId = accountIdMap.get(line.parent_id); + } + // AccountsOutputFile配列にPush + accountsOutputFileStep1Lines.push({ + // accountIdはaccountIdMapから取得する + accountId: accountIdMap.get(line.account_id), + type: line.type, + companyName: line.company_name, + country: country, + dealerAccountId: parentAccountId, + adminName: adminName, + adminMail: line.email, + userId: userIdIndex, + }); + } else { + // typeが"USER"の場合、ユーザデータの作成を行う + // userIdのインクリメント + userIdIndex = userIdIndex + 1; + // nameの変換 + // もしline.last_nameとline.first_nameが存在しない場合、line.emailをnameにする + // 存在する場合は、last_name + " " + first_name + let name = line.email; + if (line.last_name && line.first_name) { + name = `${line.last_name} ${line.first_name}`; + } + // roleの変換 + // authorIdが設定されてる場合はauthor、されていない場合は移行しないので次の行に進む + if (line.author_id) { + usersOutputFileLines.push({ + accountId: accountIdMap.get(line.account_id), + userId: userIdIndex, + name: name, + role: USER_ROLES.AUTHOR, + authorId: line.author_id, + email: line.user_email, + }); + } else { + return; + } + // ライセンスのデータの作成を行う + // authorIdが設定されてる場合、statusは"allocated"、allocated_user_idは対象のユーザID + // されていない場合、statusは"reusable"、allocated_user_idはnull + licensesOutputFileLines.push({ + expiry_date: line.expired_date.toISOString(), + account_id: accountIdMap.get(line.account_id), + type: SWITCH_FROM_TYPE.NONE, + status: line.author_id + ? LICENSE_ALLOCATED_STATUS.ALLOCATED + : LICENSE_ALLOCATED_STATUS.REUSABLE, + allocated_user_id: line.author_id ? userIdIndex : null, + }); + // WorktypesOutputFileの作成 + // wt1~wt20まで読み込み、account単位で作成する + // 作成したWorktypesOutputFileを配列にPush + for (let i = 1; i <= WORKTYPE_MAX_COUNT; i++) { + const wt = `wt${i}`; + if (line[wt]) { + // 既に存在する場合は、作成しない + if ( + worktypesOutputFileLines.find( + (worktype) => + worktype.account_id === accountIdMap.get(line.account_id) && + worktype.custom_worktype_id === line[wt].toString() + ) + ) { + continue; + } + } + } + } + // つぎの行に進む + }); + return { + accountsOutputFileStep1Lines, + usersOutputFileLines, + licensesOutputFileLines, + worktypesOutputFileLines, + }; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.registInputData.name}` + ); + } + } + + /** + * 階層の付け替えを行う + * @param accountsOutputFileStep1: AccountsOutputFileStep1[] + * @returns AccountsOutputFile[] + */ + async relocateHierarchy( + context: Context, + accountsOutputFileStep1: AccountsOutputFileStep1[] + ): Promise { + // パラメータ内容が長大なのでログには出さない + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` + ); + + try { + // dealerAccountIdを検索し、typeがCountryの場合 + accountsOutputFileStep1.forEach((account) => { + if (account.type === MIGRATION_TYPE.COUNTRY) { + console.log(account); + // そのacccountIdをdealerAccountIdにもつアカウント(Distributor)を検索する + const distributor = accountsOutputFileStep1.find( + (distributor) => distributor.accountId === account.dealerAccountId + ); + console.log(distributor); + if (distributor) { + // DistributorのdealerAccountIdをBC(Countryの親)に付け替える + distributor.dealerAccountId = account.dealerAccountId; + } + } + }); + // typeがCountryのアカウントを取り除く + accountsOutputFileStep1 = accountsOutputFileStep1.filter( + (account) => account.type !== MIGRATION_TYPE.COUNTRY + ); + + // typeをtierに変換し、AccountsOutputFileに変換する + let accountsOutputFile: AccountsOutputFile[] = []; + accountsOutputFileStep1.forEach((account) => { + let tier = 0; + switch (account.type) { + case MIGRATION_TYPE.ADMINISTRATOR: + tier = TIERS.TIER1; + break; + case MIGRATION_TYPE.BC: + tier = TIERS.TIER2; + break; + case MIGRATION_TYPE.DISTRIBUTOR: + tier = TIERS.TIER3; + break; + case MIGRATION_TYPE.DEALER: + tier = TIERS.TIER4; + break; + case MIGRATION_TYPE.CUSTOMER: + tier = TIERS.TIER5; + break; + } + accountsOutputFile.push({ + accountId: account.accountId, + type: tier, + companyName: account.companyName, + country: account.country, + dealerAccountId: account.dealerAccountId, + adminName: account.adminName, + adminMail: account.adminMail, + userId: account.userId, + }); + }); + return accountsOutputFile; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.relocateHierarchy.name}` + ); + } + } + + /** + * JSONファイルの出力 + * @param outputFilePath: string + * @param accountsOutputFile: AccountsOutputFile[] + * @param usersOutputFile: UsersOutputFile[] + * @param licensesOutputFile: LicensesOutputFile[] + * @param worktypesOutputFile: WorktypesOutputFile[] + */ + async outputJsonFile( + context: Context, + outputFilePath: string, + accountsOutputFile: AccountsOutputFile[], + usersOutputFile: UsersOutputFile[], + licensesOutputFile: LicensesOutputFile[], + worktypesOutputFile: WorktypesOutputFile[] + ): Promise { + // パラメータ内容が長大なのでログには出さない + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.outputJsonFile.name}` + ); + + try { + // JSONファイルの出力を行う + // accountsOutputFile配列の出力 + const accountsOutputFileJson = JSON.stringify(accountsOutputFile); + fs.writeFileSync( + `${outputFilePath}_accounts.json`, + accountsOutputFileJson + ); + // usersOutputFile + const usersOutputFileJson = JSON.stringify(usersOutputFile); + fs.writeFileSync(`${outputFilePath}_users.json`, usersOutputFileJson); + // licensesOutputFile + const licensesOutputFileJson = JSON.stringify(licensesOutputFile); + fs.writeFileSync( + `${outputFilePath}_licenses.json`, + licensesOutputFileJson + ); + // worktypesOutputFile + const worktypesOutputFileJson = JSON.stringify(worktypesOutputFile); + fs.writeFileSync( + `${outputFilePath}_worktypes.json`, + worktypesOutputFileJson + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.outputJsonFile.name}` + ); + } + } + + /** + * データのバリデーションチェック + * @param csvInputFile: csvInputFile[] + */ + async validateInputData( + context: Context, + csvInputFile: csvInputFile[] + ): Promise { + // パラメータ内容が長大なのでログには出さない + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.validateInputData.name}` + ); + + try { + // csvInputFileのバリデーションチェックを行う + csvInputFile.forEach((line, index) => { + // typeのバリデーションチェック + if ( + line.type !== MIGRATION_TYPE.ADMINISTRATOR && + line.type !== MIGRATION_TYPE.BC && + line.type !== MIGRATION_TYPE.COUNTRY && + line.type !== MIGRATION_TYPE.DISTRIBUTOR && + line.type !== MIGRATION_TYPE.DEALER && + line.type !== MIGRATION_TYPE.CUSTOMER && + line.type !== MIGRATION_TYPE.USER + ) { + throw new HttpException( + `type is invalid. index=${index} type=${line.type}`, + HttpStatus.BAD_REQUEST + ); + } + // countryのバリデーションチェック + if (line.country) { + if (!COUNTRY_LIST.find((country) => country.label === line.country)) { + console.log(line.country); + throw new HttpException( + `country is invalid. index=${index} country=${line.country}`, + HttpStatus.BAD_REQUEST + ); + } + } + // mailのバリデーションチェック + // メールアドレスの形式が正しいかどうかのチェック + const mailRegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (line.email) { + if (!mailRegExp.test(line.email)) { + throw new HttpException( + `email is invalid. index=${index} email=${line.email}`, + HttpStatus.BAD_REQUEST + ); + } + } + if (line.user_email) { + if (!mailRegExp.test(line.user_email)) { + throw new HttpException( + `user_email is invalid. index=${index} user_email=${line.email}`, + HttpStatus.BAD_REQUEST + ); + } + } + // recording_modeの値が存在する場合 + if (line.recording_mode) { + // recording_modeのバリデーションチェック + if ( + line.recording_mode !== RECORDING_MODE.DS2_QP && + line.recording_mode !== RECORDING_MODE.DS2_SP && + line.recording_mode !== RECORDING_MODE.DSS + ) { + throw new HttpException( + `recording_mode is invalid. index=${index} recording_mode=${line.recording_mode}`, + HttpStatus.BAD_REQUEST + ); + } + } + }); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.validateInputData.name}` + ); + } + } +} diff --git a/data_migration_tools/server/src/features/transfer/types/types.ts b/data_migration_tools/server/src/features/transfer/types/types.ts new file mode 100644 index 0000000..63eb3ab --- /dev/null +++ b/data_migration_tools/server/src/features/transfer/types/types.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + AccountsOutputFileStep1, + LicensesOutputFile, + UsersOutputFile, + WorktypesOutputFile, +} from "src/common/types/types"; + +export class transferRequest { + @ApiProperty() + inputFilePath: string; +} + +export class transferResponse {} + +export class registInputDataResponse { + @ApiProperty() + accountsOutputFileStep1Lines: AccountsOutputFileStep1[]; + @ApiProperty() + usersOutputFileLines: UsersOutputFile[]; + @ApiProperty() + licensesOutputFileLines: LicensesOutputFile[]; + @ApiProperty() + worktypesOutputFileLines: WorktypesOutputFile[]; +} diff --git a/data_migration_tools/server/src/features/users/users.controller.ts b/data_migration_tools/server/src/features/users/users.controller.ts new file mode 100644 index 0000000..14c5cad --- /dev/null +++ b/data_migration_tools/server/src/features/users/users.controller.ts @@ -0,0 +1,10 @@ +import { Controller, Logger } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { UsersService } from "./users.service"; + +@ApiTags("users") +@Controller("users") +export class UsersController { + private readonly logger = new Logger(UsersController.name); + constructor(private readonly usersService: UsersService) {} +} diff --git a/data_migration_tools/server/src/features/users/users.module.ts b/data_migration_tools/server/src/features/users/users.module.ts new file mode 100644 index 0000000..3f0da57 --- /dev/null +++ b/data_migration_tools/server/src/features/users/users.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { AdB2cModule } from "../../gateways/adb2c/adb2c.module"; +import { UsersRepositoryModule } from "../../repositories/users/users.repository.module"; +import { UsersController } from "./users.controller"; +import { UsersService } from "./users.service"; + +@Module({ + imports: [UsersRepositoryModule, AdB2cModule], + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/data_migration_tools/server/src/features/users/users.service.ts b/data_migration_tools/server/src/features/users/users.service.ts new file mode 100644 index 0000000..cb639cd --- /dev/null +++ b/data_migration_tools/server/src/features/users/users.service.ts @@ -0,0 +1,306 @@ +import { HttpException, HttpStatus, Injectable, Logger } from "@nestjs/common"; +import { makeErrorResponse } from "../../common/error/makeErrorResponse"; +import { makePassword } from "../../common/password/password"; +import { + AdB2cService, + ConflictError, + isConflictError, +} from "../../gateways/adb2c/adb2c.service"; +import { + User as EntityUser, + newUser, +} from "../../repositories/users/entity/user.entity"; +import { UsersRepositoryService } from "../../repositories/users/users.repository.service"; +import { MANUAL_RECOVERY_REQUIRED, USER_ROLES } from "../../constants"; +import { Context } from "../../common/log"; +import { UserRoles } from "../../common/types/role"; + +@Injectable() +export class UsersService { + private readonly logger = new Logger(UsersService.name); + constructor( + private readonly usersRepository: UsersRepositoryService, + private readonly adB2cService: AdB2cService + ) {} + + /** + * Creates user + * @param context + * @param name + * @param role + * @param email + * @param autoRenew + * @param notification + * @param accountId + * @param userid + * @param [authorId] + * @param [encryption] + * @param [encryptionPassword] + * @param [prompt] + * @returns user + */ + async createUser( + context: Context, + name: string, + role: UserRoles, + email: string, + autoRenew: boolean, + notification: boolean, + accountId: number, + userid: number, + authorId?: string | undefined, + encryption?: boolean | undefined, + encryptionPassword?: string | undefined, + prompt?: boolean | undefined + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.createUser.name} | params: { ` + + `role: ${role}, ` + + `autoRenew: ${autoRenew}, ` + + `notification: ${notification}, ` + + `accountId: ${accountId}, ` + + `userid: ${userid}, ` + + `authorId: ${authorId}, ` + + `encryption: ${encryption}, ` + + `prompt: ${prompt} };` + ); + + //authorIdが重複していないかチェックする + if (authorId) { + let isAuthorIdDuplicated = false; + try { + isAuthorIdDuplicated = await this.usersRepository.existsAuthorId( + context, + accountId, + authorId + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + if (isAuthorIdDuplicated) { + throw new HttpException( + makeErrorResponse("E010302"), + HttpStatus.BAD_REQUEST + ); + } + } + + // ランダムなパスワードを生成する + const ramdomPassword = makePassword(); + + //Azure AD B2Cにユーザーを新規登録する + let externalUser: { sub: string } | ConflictError; + try { + this.logger.log(`name=${name}`); + // idpにユーザーを作成 + externalUser = await this.adB2cService.createUser( + context, + email, + ramdomPassword, + name + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error( + `[${context.getTrackingId()}] create externalUser failed` + ); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + // メールアドレス重複エラー + if (isConflictError(externalUser)) { + throw new HttpException( + makeErrorResponse("E010301"), + HttpStatus.BAD_REQUEST + ); + } + + //Azure AD B2Cに登録したユーザー情報のID(sub)と受け取った情報を使ってDBにユーザーを登録する + let newUser: EntityUser; + + try { + //roleに応じてユーザー情報を作成する + const newUserInfo = this.createNewUserInfo( + context, + userid, + role, + accountId, + externalUser.sub, + autoRenew, + notification, + authorId, + encryption, + encryptionPassword, + prompt + ); + // ユーザ作成 + newUser = await this.usersRepository.createNormalUser( + context, + newUserInfo + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error(`[${context.getTrackingId()}]create user failed`); + //リカバリー処理 + //Azure AD B2Cに登録したユーザー情報を削除する + await this.deleteB2cUser(externalUser.sub, context); + + switch (e.code) { + case "ER_DUP_ENTRY": + //AuthorID重複エラー + throw new HttpException( + makeErrorResponse("E010302"), + HttpStatus.BAD_REQUEST + ); + default: + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createUser.name}` + ); + return; + } + + // Azure AD B2Cに登録したユーザー情報を削除する + // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 + private async deleteB2cUser(externalUserId: string, context: Context) { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteB2cUser.name + } | params: { externalUserId: ${externalUserId} }` + ); + try { + await this.adB2cService.deleteUser(externalUserId, context); + this.logger.log( + `[${context.getTrackingId()}] delete externalUser: ${externalUserId}` + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${externalUserId}` + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}` + ); + } + } + + // DBに登録したユーザー情報を削除する + private async deleteUser(userId: number, context: Context) { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteUser.name + } | params: { userId: ${userId} }` + ); + try { + await this.usersRepository.deleteNormalUser(context, userId); + this.logger.log(`[${context.getTrackingId()}] delete user: ${userId}`); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete user: ${userId}` + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}` + ); + } + } + + // roleを受け取って、roleに応じたnewUserを作成して返却する + private createNewUserInfo( + context: Context, + id: number, + role: UserRoles, + accountId: number, + externalId: string, + autoRenew: boolean, + notification: boolean, + authorId?: string | undefined, + encryption?: boolean | undefined, + encryptionPassword?: string | undefined, + prompt?: boolean | undefined + ): newUser { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.createNewUserInfo.name + } | params: { ` + + `id: ${id}, ` + + `role: ${role}, ` + + `accountId: ${accountId}, ` + + `authorId: ${authorId}, ` + + `externalId: ${externalId}, ` + + `autoRenew: ${autoRenew}, ` + + `notification: ${notification}, ` + + `authorId: ${authorId}, ` + + `encryption: ${encryption}, ` + + `prompt: ${prompt} };` + ); + try { + switch (role) { + case USER_ROLES.NONE: + case USER_ROLES.TYPIST: + return { + id, + account_id: accountId, + external_id: externalId, + auto_renew: autoRenew, + notification, + role, + accepted_dpa_version: null, + accepted_eula_version: null, + accepted_privacy_notice_version: null, + encryption: false, + encryption_password: null, + prompt: false, + author_id: null, + }; + case USER_ROLES.AUTHOR: + return { + id, + account_id: accountId, + external_id: externalId, + auto_renew: autoRenew, + notification, + role, + author_id: authorId ?? null, + encryption: encryption ?? false, + encryption_password: encryptionPassword ?? null, + prompt: prompt ?? false, + accepted_dpa_version: null, + accepted_eula_version: null, + accepted_privacy_notice_version: null, + }; + default: + //不正なroleが指定された場合はログを出力してエラーを返す + this.logger.error( + `[${context.getTrackingId()}] [NOT IMPLEMENT] [RECOVER] role: ${role}` + ); + throw new HttpException( + makeErrorResponse("E009999"), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + return e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createNewUserInfo.name}` + ); + } + } +} diff --git a/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts b/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts index 743fbf8..5ba0f0e 100644 --- a/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts +++ b/data_migration_tools/server/src/gateways/adb2c/adb2c.service.ts @@ -5,6 +5,8 @@ import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { AdB2cResponse, AdB2cUser } from "./types/types"; import { isPromiseRejectedResult } from "./utils/utils"; +import { Context } from "../../common/log"; +import { ADB2C_SIGN_IN_TYPE } from "../../constants"; export type ConflictError = { reason: "email"; @@ -27,9 +29,12 @@ export const isConflictError = (arg: unknown): arg is ConflictError => { @Injectable() export class AdB2cService { private readonly logger = new Logger(AdB2cService.name); + private readonly tenantName: string; private graphClient: Client; constructor(private readonly configService: ConfigService) { + this.tenantName = this.configService.getOrThrow("TENANT_NAME"); + // ADB2Cへの認証情報 const credential = new ClientSecretCredential( this.configService.getOrThrow("ADB2C_TENANT_ID"), @@ -42,13 +47,70 @@ export class AdB2cService { this.graphClient = Client.initWithMiddleware({ authProvider }); } + + /** + * Creates user AzureADB2Cにユーザーを追加する + * @param email 管理ユーザーのメールアドレス + * @param password 管理ユーザーのパスワード + * @param username 管理ユーザーの名前 + * @returns user + */ + async createUser( + context: Context, + email: string, + password: string, + username: string + ): Promise<{ sub: string } | ConflictError> { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.createUser.name}` + ); + try { + // ユーザをADB2Cに登録 + const newUser = await this.graphClient.api("users/").post({ + accountEnabled: true, + displayName: username, + passwordPolicies: "DisableStrongPassword", + passwordProfile: { + forceChangePasswordNextSignIn: false, + password: password, + }, + identities: [ + { + signinType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: `${this.tenantName}.onmicrosoft.com`, + issuerAssignedId: email, + }, + ], + }); + return { sub: newUser.id }; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e?.statusCode === 400 && e?.body) { + const error = JSON.parse(e.body); + + // エラーが競合エラーである場合は、メールアドレス重複としてエラーを返す + if (error?.details?.find((x) => x.code === "ObjectConflict")) { + return { reason: "email", message: "ObjectConflict" }; + } + } + + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createUser.name}` + ); + } + } + /** * Gets users * @param externalIds * @returns users */ - async getUsers(): Promise { - this.logger.log(`[IN] ${this.getUsers.name}`); + async getUsers( + context: Context + ): Promise<{ users: AdB2cUser[]; hasNext: boolean }> { + this.logger.log(`[IN] [${context.getTrackingId()}] ${this.getUsers.name}`); try { const res: AdB2cResponse = await this.graphClient @@ -57,7 +119,7 @@ export class AdB2cService { .filter(`creationType eq 'LocalAccount'`) .get(); - return res.value; + return { users: res.value, hasNext: !!res["@odata.nextLink"] }; } catch (e) { this.logger.error(`error=${e}`); const { statusCode } = e; @@ -71,13 +133,53 @@ export class AdB2cService { } } + /** + * Azure AD B2Cからユーザ情報を削除する + * @param externalId 外部ユーザーID + * @param context コンテキスト + */ + async deleteUser(externalId: string, context: Context): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteUser.name + } | params: { externalId: ${externalId} };` + ); + + try { + // https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example + await this.graphClient.api(`users/${externalId}`).delete(); + this.logger.log( + `[${context.getTrackingId()}] [ADB2C DELETE] externalId: ${externalId}` + ); + + // キャッシュからも削除する + // 移行ツール特別対応:キャッシュ登録は行わないので削除も不要 + /* + try { + await this.redisService.del(context, makeADB2CKey(externalId)); + } catch (e) { + // キャッシュからの削除に失敗しても、ADB2Cからの削除は成功しているため例外はスローしない + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } + */ + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}` + ); + } + } /** * Azure AD B2Cからユーザ情報を削除する(複数) * @param externalIds 外部ユーザーID */ - async deleteUsers(externalIds: string[]): Promise { + async deleteUsers(context: Context, externalIds: string[]): Promise { this.logger.log( - `[IN]${this.deleteUsers.name} | params: { externalIds: ${externalIds} };` + `[IN] [${context.getTrackingId()}] ${ + this.deleteUsers.name + } | params: { externalIds: ${externalIds} };` ); try { diff --git a/data_migration_tools/server/src/gateways/blobstorage/blobstorage.service.ts b/data_migration_tools/server/src/gateways/blobstorage/blobstorage.service.ts index 68e5a80..ee603c6 100644 --- a/data_migration_tools/server/src/gateways/blobstorage/blobstorage.service.ts +++ b/data_migration_tools/server/src/gateways/blobstorage/blobstorage.service.ts @@ -1,10 +1,16 @@ import { Injectable, Logger } from "@nestjs/common"; import { + ContainerClient, BlobServiceClient, StorageSharedKeyCredential, } from "@azure/storage-blob"; +import { + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, +} from "../../constants"; import { ConfigService } from "@nestjs/config"; - +import { Context } from "../../common/log"; @Injectable() export class BlobstorageService { private readonly logger = new Logger(BlobstorageService.name); @@ -41,12 +47,52 @@ export class BlobstorageService { ); } + /** + * Creates container + * @param context + * @param accountId + * @param country + * @returns container + */ + async createContainer( + context: Context, + accountId: number, + country: string + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.createContainer.name + } | params: { ` + `accountId: ${accountId} };` + ); + + // 国に応じたリージョンでコンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient( + context, + accountId, + country + ); + + try { + // コンテナ作成 + await containerClient.create(); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.createContainer.name}` + ); + } + } + /** * すべてのコンテナを削除します。 * @returns containers */ - async deleteContainers(): Promise { - this.logger.log(`[IN] ${this.deleteContainers.name}`); + async deleteContainers(context: Context): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.deleteContainers.name}` + ); try { for await (const container of this.blobServiceClientAU.listContainers({ @@ -80,4 +126,32 @@ export class BlobstorageService { this.logger.log(`[OUT] ${this.deleteContainers.name}`); } } + + /** + * Gets container client + * @param companyName + * @returns container client + */ + private getContainerClient( + context: Context, + accountId: number, + country: string + ): ContainerClient { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.getContainerClient.name + } | params: { ` + `accountId: ${accountId} };` + ); + + const containerName = `account-${accountId}`; + if (BLOB_STORAGE_REGION_US.includes(country)) { + return this.blobServiceClientUS.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_AU.includes(country)) { + return this.blobServiceClientAU.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return this.blobServiceClientEU.getContainerClient(containerName); + } else { + throw new Error("invalid country"); + } + } } diff --git a/data_migration_tools/server/src/repositories/accounts/accounts.repository.module.ts b/data_migration_tools/server/src/repositories/accounts/accounts.repository.module.ts new file mode 100644 index 0000000..ddd0efd --- /dev/null +++ b/data_migration_tools/server/src/repositories/accounts/accounts.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Account } from './entity/account.entity'; +import { AccountsRepositoryService } from './accounts.repository.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Account])], + providers: [AccountsRepositoryService], + exports: [AccountsRepositoryService], +}) +export class AccountsRepositoryModule {} diff --git a/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts b/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts new file mode 100644 index 0000000..ec24808 --- /dev/null +++ b/data_migration_tools/server/src/repositories/accounts/accounts.repository.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from '@nestjs/common'; +import { + DataSource, +} from 'typeorm'; +import { User } from '../users/entity/user.entity'; +import { Account } from './entity/account.entity'; +import { + getDirection, + getTaskListSortableAttribute, +} from '../../common/types/sort/util'; +import { SortCriteria } from "../sort_criteria/entity/sort_criteria.entity"; +import { + insertEntity, + updateEntity, + deleteEntity, +} from '../../common/repository'; +import { Context } from '../../common/log'; + +@Injectable() +export class AccountsRepositoryService { + // クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== "local"; + constructor(private dataSource: DataSource) {} + + /** + * プライマリ管理者とアカウント、ソート条件を同時に作成する + * @param companyName + * @param country + * @param dealerAccountId + * @param tier + * @param adminExternalUserId + * @param adminUserRole + * @param accountId + * @param userId + * @param adminUserAcceptedEulaVersion + * @param adminUserAcceptedPrivacyNoticeVersion + * @param adminUserAcceptedDpaVersion + * @returns account/admin user + */ + async createAccount( + context: Context, + companyName: string, + country: string, + dealerAccountId: number | undefined, + tier: number, + adminExternalUserId: string, + adminUserRole: string, + accountId: number, + userId: number, + adminUserAcceptedEulaVersion?: string, + adminUserAcceptedPrivacyNoticeVersion?: string, + adminUserAcceptedDpaVersion?: string + ): Promise<{ newAccount: Account; adminUser: User }> { + return await this.dataSource.transaction(async (entityManager) => { + const account = new Account(); + { + account.id = accountId; + account.parent_account_id = dealerAccountId ?? null; + account.company_name = companyName; + account.country = country; + account.tier = tier; + } + const accountsRepo = entityManager.getRepository(Account); + const newAccount = accountsRepo.create(account); + const persistedAccount = await insertEntity( + Account, + accountsRepo, + newAccount, + this.isCommentOut, + context + ); + + // 作成されたAccountのIDを使用してユーザーを作成 + const user = new User(); + { + user.id = userId; + user.account_id = persistedAccount.id; + user.external_id = adminExternalUserId; + user.role = adminUserRole; + user.accepted_eula_version = adminUserAcceptedEulaVersion ?? null; + user.accepted_privacy_notice_version = + adminUserAcceptedPrivacyNoticeVersion ?? null; + user.accepted_dpa_version = adminUserAcceptedDpaVersion ?? null; + } + const usersRepo = entityManager.getRepository(User); + const newUser = usersRepo.create(user); + const persistedUser = await insertEntity( + User, + usersRepo, + newUser, + this.isCommentOut, + context + ); + + // アカウントに管理者を設定して更新 + persistedAccount.primary_admin_user_id = persistedUser.id; + + const result = await updateEntity( + accountsRepo, + { id: persistedAccount.id }, + persistedAccount, + this.isCommentOut, + context + ); + + // 想定外の更新が行われた場合はロールバックを行った上でエラー送出 + if (result.affected !== 1) { + throw new Error(`invalid update. result.affected=${result.affected}`); + } + + // ユーザーのタスクソート条件を作成 + const sortCriteria = new SortCriteria(); + { + sortCriteria.parameter = getTaskListSortableAttribute("JOB_NUMBER"); + sortCriteria.direction = getDirection("ASC"); + sortCriteria.user_id = persistedUser.id; + } + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const newSortCriteria = sortCriteriaRepo.create(sortCriteria); + await insertEntity( + SortCriteria, + sortCriteriaRepo, + newSortCriteria, + this.isCommentOut, + context + ); + + return { newAccount: persistedAccount, adminUser: persistedUser }; + }); + } + + /** + * プライマリ管理者とアカウント、ソート条件を同時に削除する + * @param accountId + * @returns delete + */ + async deleteAccount( + context: Context, + accountId: number, + userId: number + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const accountsRepo = entityManager.getRepository(Account); + const usersRepo = entityManager.getRepository(User); + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + // ソート条件を削除 + await deleteEntity( + sortCriteriaRepo, + { user_id: userId }, + this.isCommentOut, + context + ); + // プライマリ管理者を削除 + await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context); + // アカウントを削除 + await deleteEntity( + accountsRepo, + { id: accountId }, + this.isCommentOut, + context + ); + }); + } +} diff --git a/data_migration_tools/server/src/repositories/accounts/entity/account.entity.ts b/data_migration_tools/server/src/repositories/accounts/entity/account.entity.ts new file mode 100644 index 0000000..3c40a03 --- /dev/null +++ b/data_migration_tools/server/src/repositories/accounts/entity/account.entity.ts @@ -0,0 +1,70 @@ +import { bigintTransformer } from '../../../common/entity'; +import { User } from '../../../repositories/users/entity/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; +} diff --git a/data_migration_tools/server/src/repositories/accounts/errors/types.ts b/data_migration_tools/server/src/repositories/accounts/errors/types.ts new file mode 100644 index 0000000..826700c --- /dev/null +++ b/data_migration_tools/server/src/repositories/accounts/errors/types.ts @@ -0,0 +1,28 @@ +// アカウント未発見エラー +export class AccountNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'AccountNotFoundError'; + } +} +// ディーラーアカウント未存在エラー +export class DealerAccountNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'DealerAccountNotFoundError'; + } +} +// 管理者ユーザ未存在エラー +export class AdminUserNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'AdminUserNotFoundError'; + } +} +// アカウントロックエラー +export class AccountLockedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AccountLockedError'; + } +} diff --git a/data_migration_tools/server/src/repositories/delete/delete.repository.service.ts b/data_migration_tools/server/src/repositories/delete/delete.repository.service.ts index 52539c0..f436207 100644 --- a/data_migration_tools/server/src/repositories/delete/delete.repository.service.ts +++ b/data_migration_tools/server/src/repositories/delete/delete.repository.service.ts @@ -1,11 +1,15 @@ 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"; +import { Term } from "./entity/term.entity"; +import { insertEntities } from "../../common/repository"; +import { Context } from "../../common/log"; @Injectable() export class DeleteRepositoryService { + // クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== "local"; constructor(private dataSource: DataSource) {} /** @@ -54,4 +58,35 @@ export class DeleteRepositoryService { await queryRunner.release(); } } + + /** + * 初期データを挿入する + * @returns data + */ + async insertInitData(context: Context): Promise { + await this.dataSource.transaction(async (entityManager) => { + const termRepo = entityManager.getRepository(Term); + + // ワークフローのデータ作成 + const newTarmDpa = new Term(); + newTarmDpa.document_type = "DPA"; + newTarmDpa.version = "V0.1"; + const newTarmEula = new Term(); + newTarmEula.document_type = "EULA"; + newTarmEula.version = "V0.1"; + const newTarmPrivacyNotice = new Term(); + newTarmPrivacyNotice.document_type = "PrivacyNotice"; + newTarmPrivacyNotice.version = "V0.1"; + + const initTerms = [newTarmDpa, newTarmEula, newTarmPrivacyNotice]; + + await insertEntities( + Term, + termRepo, + initTerms, + this.isCommentOut, + context + ); + }); + } } diff --git a/data_migration_tools/server/src/repositories/licenses/entity/license.entity.ts b/data_migration_tools/server/src/repositories/licenses/entity/license.entity.ts new file mode 100644 index 0000000..90715ae --- /dev/null +++ b/data_migration_tools/server/src/repositories/licenses/entity/license.entity.ts @@ -0,0 +1,137 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { bigintTransformer } from '../../../common/entity'; + + +@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; +} + +@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; + +} + + +@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; +} \ No newline at end of file diff --git a/data_migration_tools/server/src/repositories/licenses/errors/types.ts b/data_migration_tools/server/src/repositories/licenses/errors/types.ts new file mode 100644 index 0000000..12b34d1 --- /dev/null +++ b/data_migration_tools/server/src/repositories/licenses/errors/types.ts @@ -0,0 +1,108 @@ +// POナンバーがすでに存在するエラー +export class PoNumberAlreadyExistError extends Error { + constructor(message: string) { + super(message); + this.name = 'PoNumberAlreadyExistError'; + } +} + +// 取り込むカードライセンスが存在しないエラー +export class LicenseNotExistError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseNotExistError'; + } +} + +// 取り込むライセンスが既に取り込み済みのエラー +export class LicenseKeyAlreadyActivatedError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseKeyAlreadyActivatedError'; + } +} + +// 注文不在エラー +export class OrderNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'OrderNotFoundError'; + } +} +// 注文発行済エラー +export class AlreadyIssuedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AlreadyIssuedError'; + } +} +// ライセンス不足エラー +export class LicensesShortageError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicensesShortageError'; + } +} + +// ライセンス有効期限切れエラー +export class LicenseExpiredError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseExpiredError'; + } +} +// ライセンス割り当て不可エラー +export class LicenseUnavailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseUnavailableError'; + } +} + +// ライセンス割り当て解除済みエラー +export class LicenseAlreadyDeallocatedError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseAlreadyDeallocatedError'; + } +} + +// 注文キャンセル失敗エラー +export class CancelOrderFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'CancelOrderFailedError'; + } +} + +// ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) +export class AlreadyLicenseStatusChangedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AlreadyLicenseStatusChangedError'; + } +} + +// ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) +export class CancellationPeriodExpiredError extends Error { + constructor(message: string) { + super(message); + this.name = 'CancellationPeriodExpiredError'; + } +} + +// ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) +export class AlreadyLicenseAllocatedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AlreadyLicenseAllocatedError'; + } +} + +// ライセンス未割当エラー +export class LicenseNotAllocatedError extends Error { + constructor(message: string) { + super(message); + this.name = 'LicenseNotAllocatedError'; + } +} diff --git a/data_migration_tools/server/src/repositories/licenses/licenses.repository.module.ts b/data_migration_tools/server/src/repositories/licenses/licenses.repository.module.ts new file mode 100644 index 0000000..e3e3d0c --- /dev/null +++ b/data_migration_tools/server/src/repositories/licenses/licenses.repository.module.ts @@ -0,0 +1,17 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { + CardLicense, + License, + LicenseAllocationHistory, +} from "./entity/license.entity"; +import { LicensesRepositoryService } from "./licenses.repository.service"; + +@Module({ + imports: [ + TypeOrmModule.forFeature([License, CardLicense, LicenseAllocationHistory]), + ], + providers: [LicensesRepositoryService], + exports: [LicensesRepositoryService], +}) +export class LicensesRepositoryModule {} diff --git a/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts b/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts new file mode 100644 index 0000000..52e13b5 --- /dev/null +++ b/data_migration_tools/server/src/repositories/licenses/licenses.repository.service.ts @@ -0,0 +1,130 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { DataSource, In } from "typeorm"; +import { + License, + LicenseAllocationHistory, + CardLicense, +} from "./entity/license.entity"; +import { insertEntities } from "../../common/repository"; +import { Context } from "../../common/log"; +import { + LicensesInputFile, + CardLicensesInputFile, +} from "../../common/types/types"; + +@Injectable() +export class LicensesRepositoryService { + //クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== "local"; + constructor(private dataSource: DataSource) {} + private readonly logger = new Logger(LicensesRepositoryService.name); + + /** + * ライセンスを登録する + * @context Context + * @param licensesInputFiles + */ + async insertLicenses( + context: Context, + licensesInputFiles: LicensesInputFile[] + ): Promise<{}> { + const nowDate = new Date(); + return await this.dataSource.transaction(async (entityManager) => { + const licenseRepo = entityManager.getRepository(License); + + let newLicenses: License[] = []; + licensesInputFiles.forEach((licensesInputFile) => { + const license = new License(); + license.account_id = licensesInputFile.account_id; + license.status = licensesInputFile.status; + license.type = licensesInputFile.type; + license.expiry_date = (licensesInputFile.expiry_date) ? new Date(licensesInputFile.expiry_date) : null; + if (licensesInputFile.allocated_user_id) { + license.allocated_user_id = licensesInputFile.allocated_user_id; + } + newLicenses.push(license); + }); + + // ライセンステーブルを登録 + const insertedlicenses = await insertEntities( + License, + licenseRepo, + newLicenses, + this.isCommentOut, + context + ); + + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory + ); + // ユーザに割り当てた場合はライセンス割り当て履歴にも登録 + let newLicenseAllocationHistories: LicenseAllocationHistory[] = []; + insertedlicenses.forEach((insertedlicense) => { + if (insertedlicense.allocated_user_id) { + const licenseAllocationHistory = new LicenseAllocationHistory(); + licenseAllocationHistory.user_id = insertedlicense.allocated_user_id; + licenseAllocationHistory.license_id = insertedlicense.id; + licenseAllocationHistory.is_allocated = true; + licenseAllocationHistory.account_id = insertedlicense.account_id; + licenseAllocationHistory.switch_from_type = "NONE"; + licenseAllocationHistory.executed_at = insertedlicense.created_at; + + newLicenseAllocationHistories.push(licenseAllocationHistory); + } + }); + + // ライセンス割り当てテーブルを登録 + await insertEntities( + LicenseAllocationHistory, + licenseAllocationHistoryRepo, + newLicenseAllocationHistories, + this.isCommentOut, + context + ); + + return {}; + }); + } + + /** + * カードライセンスを登録する + * @context Context + * @param cardLicensesInputFiles + */ + async insertCardLicenses( + context: Context, + cardLicensesInputFiles: CardLicensesInputFile[] + ): Promise<{}> { + return await this.dataSource.transaction(async (entityManager) => { + const cardLicenseRepo = entityManager.getRepository(CardLicense); + + + let newCardLicenses: CardLicense[] = []; + cardLicensesInputFiles.forEach((cardLicensesInputFile) => { + const cardLicense = new CardLicense(); + cardLicense.license_id = cardLicensesInputFile.license_id; + cardLicense.issue_id = cardLicensesInputFile.issue_id; + cardLicense.card_license_key = cardLicensesInputFile.card_license_key; + cardLicense.activated_at = (cardLicensesInputFile.activated_at) ? new Date(cardLicensesInputFile.activated_at) : null; + cardLicense.created_at = (cardLicensesInputFile.created_at) ? new Date(cardLicensesInputFile.created_at) : null; + cardLicense.created_by = cardLicensesInputFile.created_by; + cardLicense.updated_at = (cardLicensesInputFile.updated_at) ? new Date(cardLicensesInputFile.updated_at) : null; + cardLicense.updated_by = cardLicensesInputFile.updated_by; + + newCardLicenses.push(cardLicense); + }); + + const query = cardLicenseRepo + .createQueryBuilder() + .insert() + .into(CardLicense); + if (this.isCommentOut) { + query.comment(`${context.getTrackingId()}_${new Date().toUTCString()}`); + } + query.values(newCardLicenses).execute(); + + return {}; + }); + } + +} diff --git a/data_migration_tools/server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts b/data_migration_tools/server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts new file mode 100644 index 0000000..260c9a9 --- /dev/null +++ b/data_migration_tools/server/src/repositories/sort_criteria/entity/sort_criteria.entity.ts @@ -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; +} diff --git a/data_migration_tools/server/src/repositories/sort_criteria/sort_criteria.repository.module.ts b/data_migration_tools/server/src/repositories/sort_criteria/sort_criteria.repository.module.ts new file mode 100644 index 0000000..8a46b37 --- /dev/null +++ b/data_migration_tools/server/src/repositories/sort_criteria/sort_criteria.repository.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SortCriteria } from './entity/sort_criteria.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SortCriteria])], +}) +export class SortCriteriaRepositoryModule {} diff --git a/data_migration_tools/server/src/repositories/users/entity/user.entity.ts b/data_migration_tools/server/src/repositories/users/entity/user.entity.ts new file mode 100644 index 0000000..9ae5eb6 --- /dev/null +++ b/data_migration_tools/server/src/repositories/users/entity/user.entity.ts @@ -0,0 +1,97 @@ +import { Account } from '../../../repositories/accounts/entity/account.entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} 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_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; + +} + + +export type newUser = Omit< + User, + | 'deleted_at' + | 'created_at' + | 'updated_at' + | 'updated_by' + | 'created_by' + | 'account' + | 'license' + | 'userGroupMembers' + | 'email_verified' +>; diff --git a/data_migration_tools/server/src/repositories/users/errors/types.ts b/data_migration_tools/server/src/repositories/users/errors/types.ts new file mode 100644 index 0000000..6f90ede --- /dev/null +++ b/data_migration_tools/server/src/repositories/users/errors/types.ts @@ -0,0 +1,56 @@ +// Email検証済みエラー +export class EmailAlreadyVerifiedError extends Error { + constructor(message: string) { + super(message); + this.name = 'EmailAlreadyVerifiedError'; + } +} +// ユーザー未発見エラー +export class UserNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'UserNotFoundError'; + } +} +// AuthorID重複エラー +export class AuthorIdAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuthorIdAlreadyExistsError'; + } +} +// 不正なRole変更エラー +export class InvalidRoleChangeError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidRoleChangeError'; + } +} +// 暗号化パスワード不足エラー +export class EncryptionPasswordNeedError extends Error { + constructor(message: string) { + super(message); + this.name = 'EncryptionPasswordNeedError'; + } +} +// 利用規約バージョン情報不在エラー +export class TermInfoNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'TermInfoNotFoundError'; + } +} +// 利用規約バージョンパラメータ不在エラー +export class UpdateTermsVersionNotSetError extends Error { + constructor(message: string) { + super(message); + this.name = 'UpdateTermsVersionNotSetError'; + } +} +// 代行操作不許可エラー +export class DelegationNotAllowedError extends Error { + constructor(message: string) { + super(message); + this.name = 'DelegationNotAllowedError'; + } +} diff --git a/data_migration_tools/server/src/repositories/users/users.repository.module.ts b/data_migration_tools/server/src/repositories/users/users.repository.module.ts new file mode 100644 index 0000000..79be43a --- /dev/null +++ b/data_migration_tools/server/src/repositories/users/users.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './entity/user.entity'; +import { UsersRepositoryService } from './users.repository.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + providers: [UsersRepositoryService], + exports: [UsersRepositoryService], +}) +export class UsersRepositoryModule {} diff --git a/data_migration_tools/server/src/repositories/users/users.repository.service.ts b/data_migration_tools/server/src/repositories/users/users.repository.service.ts new file mode 100644 index 0000000..ebb4dc3 --- /dev/null +++ b/data_migration_tools/server/src/repositories/users/users.repository.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@nestjs/common'; +import { User, newUser } from './entity/user.entity'; +import { + DataSource, +} from 'typeorm'; +import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity'; +import { + getDirection, + getTaskListSortableAttribute, +} from '../../common/types/sort/util'; +import { Context } from '../../common/log'; +import { + insertEntity, + deleteEntity, +} from '../../common/repository'; + +@Injectable() +export class UsersRepositoryService { + // クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== "local"; + + constructor(private dataSource: DataSource) {} + + /** + * 一般ユーザーを作成する + * @param user + * @returns User + */ + async createNormalUser(context: Context, user: newUser): Promise { + const { + id, + account_id: accountId, + external_id: externalUserId, + role, + auto_renew, + notification, + author_id, + accepted_eula_version, + accepted_dpa_version, + encryption, + encryption_password: encryptionPassword, + prompt, + } = user; + const userEntity = new User(); + + userEntity.id = id; + userEntity.role = role; + userEntity.account_id = accountId; + userEntity.external_id = externalUserId; + userEntity.auto_renew = auto_renew; + userEntity.notification = notification; + userEntity.author_id = author_id; + userEntity.accepted_eula_version = accepted_eula_version; + userEntity.accepted_dpa_version = accepted_dpa_version; + userEntity.encryption = encryption; + userEntity.encryption_password = encryptionPassword; + userEntity.prompt = prompt; + userEntity.email_verified = true; + + const createdEntity = await this.dataSource.transaction( + async (entityManager) => { + const repo = entityManager.getRepository(User); + const newUser = repo.create(userEntity); + const persisted = await insertEntity( + User, + repo, + newUser, + this.isCommentOut, + context + ); + + // ユーザーのタスクソート条件を作成 + const sortCriteria = new SortCriteria(); + { + sortCriteria.parameter = getTaskListSortableAttribute("JOB_NUMBER"); + sortCriteria.direction = getDirection("ASC"); + sortCriteria.user_id = persisted.id; + } + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const newSortCriteria = sortCriteriaRepo.create(sortCriteria); + await insertEntity( + SortCriteria, + sortCriteriaRepo, + newSortCriteria, + this.isCommentOut, + context + ); + + return persisted; + } + ); + return createdEntity; + } + + /** + * AuthorIdが既に存在するか確認する + * @param user + * @returns 存在する:true 存在しない:false + */ + async existsAuthorId( + context: Context, + accountId: number, + authorId: string + ): Promise { + const user = await this.dataSource.getRepository(User).findOne({ + where: [ + { + account_id: accountId, + author_id: authorId, + }, + ], + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (user) { + return true; + } + return false; + } + + /** + * UserID指定のユーザーとソート条件を同時に削除する + * @param userId + * @returns delete + */ + async deleteNormalUser(context: Context, userId: number): Promise { + await this.dataSource.transaction(async (entityManager) => { + const usersRepo = entityManager.getRepository(User); + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + // ソート条件を削除 + await deleteEntity( + sortCriteriaRepo, + { user_id: userId }, + this.isCommentOut, + context + ); + // プライマリ管理者を削除 + await deleteEntity(usersRepo, { id: userId }, this.isCommentOut, context); + }); + } +} diff --git a/data_migration_tools/server/src/repositories/worktypes/entity/option_item.entity.ts b/data_migration_tools/server/src/repositories/worktypes/entity/option_item.entity.ts new file mode 100644 index 0000000..f9e7ac4 --- /dev/null +++ b/data_migration_tools/server/src/repositories/worktypes/entity/option_item.entity.ts @@ -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; +} diff --git a/data_migration_tools/server/src/repositories/worktypes/entity/worktype.entity.ts b/data_migration_tools/server/src/repositories/worktypes/entity/worktype.entity.ts new file mode 100644 index 0000000..6419cbd --- /dev/null +++ b/data_migration_tools/server/src/repositories/worktypes/entity/worktype.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@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; + +} diff --git a/data_migration_tools/server/src/repositories/worktypes/errors/types.ts b/data_migration_tools/server/src/repositories/worktypes/errors/types.ts new file mode 100644 index 0000000..fb5db2d --- /dev/null +++ b/data_migration_tools/server/src/repositories/worktypes/errors/types.ts @@ -0,0 +1,28 @@ +// WorktypeID重複エラー +export class WorktypeIdAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorktypeIdAlreadyExistsError'; + } +} +// WorktypeID登録上限エラー +export class WorktypeIdMaxCountError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorktypeIdMaxCountError'; + } +} +// WorktypeID不在エラー +export class WorktypeIdNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorktypeIdNotFoundError'; + } +} +// WorktypeID使用中エラー +export class WorktypeIdInUseError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorktypeIdInUseError'; + } +} diff --git a/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.module.ts b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.module.ts new file mode 100644 index 0000000..1ae34d1 --- /dev/null +++ b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Worktype } from './entity/worktype.entity'; +import { WorktypesRepositoryService } from './worktypes.repository.service'; +import { OptionItem } from "./entity/option_item.entity"; + +@Module({ + imports: [TypeOrmModule.forFeature([Worktype, OptionItem])], + providers: [WorktypesRepositoryService], + exports: [WorktypesRepositoryService], +}) +export class WorktypesRepositoryModule {} diff --git a/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts new file mode 100644 index 0000000..9f7f2ff --- /dev/null +++ b/data_migration_tools/server/src/repositories/worktypes/worktypes.repository.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from "@nestjs/common"; +import { DataSource, Not } from "typeorm"; +import { Worktype } from "./entity/worktype.entity"; +import { + OPTION_ITEM_NUM, + OPTION_ITEM_VALUE_TYPE, + WORKTYPE_MAX_COUNT, +} from "../../constants"; +import { + WorktypeIdAlreadyExistsError, + WorktypeIdMaxCountError, +} from "./errors/types"; +import { OptionItem } from "./entity/option_item.entity"; +import { insertEntities, insertEntity } from "../../common/repository"; +import { Context } from "../../common/log"; +import { WorktypesInputFile } from "../../common/types/types"; + +@Injectable() +export class WorktypesRepositoryService { + // クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== "local"; + + constructor(private dataSource: DataSource) {} + + /** + * ワークタイプを作成する + * @param accountId + * @param worktypeId + * @param [description] + */ + async createWorktype( + context: Context, + worktypesInputFiles: WorktypesInputFile[] + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const worktypeRepo = entityManager.getRepository(Worktype); + const optionItemRepo = entityManager.getRepository(OptionItem); + + for (const worktypesInputFile of worktypesInputFiles) { + const accountId = worktypesInputFile.account_id; + const worktypeId = worktypesInputFile.custom_worktype_id; + const description = null; + + const duplicatedWorktype = await worktypeRepo.findOne({ + where: { account_id: accountId, custom_worktype_id: worktypeId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: "pessimistic_write" }, + }); + + // ワークタイプIDが重複している場合はエラー + if (duplicatedWorktype) { + throw new WorktypeIdAlreadyExistsError( + `WorktypeID is already exists. WorktypeID: ${worktypeId}` + ); + } + + const worktypeCount = await worktypeRepo.count({ + where: { account_id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: "pessimistic_write" }, + }); + + // ワークタイプの登録数が上限に達している場合はエラー + if (worktypeCount >= WORKTYPE_MAX_COUNT) { + throw new WorktypeIdMaxCountError( + `Number of worktype is exceeded the limit. MAX_COUNT: ${WORKTYPE_MAX_COUNT}, currentCount: ${worktypeCount}` + ); + } + + // ワークタイプを作成 + const worktype = await insertEntity( + Worktype, + worktypeRepo, + { + account_id: accountId, + custom_worktype_id: worktypeId, + description: description ?? null, + }, + this.isCommentOut, + context + ); + + // ワークタイプに紐づくオプションアイテムを10件作成 + const newOptionItems = Array.from({ length: OPTION_ITEM_NUM }, () => { + const optionItem = new OptionItem(); + optionItem.worktype_id = worktype.id; + optionItem.item_label = ""; + optionItem.default_value_type = OPTION_ITEM_VALUE_TYPE.DEFAULT; + optionItem.initial_value = ""; + + return optionItem; + }); + + await insertEntities( + OptionItem, + optionItemRepo, + newOptionItems, + this.isCommentOut, + context + ); + } + }); + } +} diff --git a/dictation_server/src/common/validators/authorId.validator.ts b/dictation_server/src/common/validators/authorId.validator.ts index 02ad278..23afafb 100644 --- a/dictation_server/src/common/validators/authorId.validator.ts +++ b/dictation_server/src/common/validators/authorId.validator.ts @@ -5,11 +5,26 @@ import { ValidationOptions, registerDecorator, } from 'class-validator'; +import { + PostUpdateUserRequest, + SignupRequest, +} from '../../features/users/types/types'; +import { USER_ROLES } from '../../constants'; // 大文字英数字とアンダースコアのみを許可するバリデータ @ValidatorConstraint({ name: 'IsAuthorId', async: false }) export class IsAuthorId implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { + const request = args.object as SignupRequest | PostUpdateUserRequest; + // requestの存在チェック + if (!request) { + return false; + } + const { role } = request; + // roleがauthor以外の場合はスキップする + if (role !== USER_ROLES.AUTHOR) { + return true; + } return /^[A-Z0-9_]*$/.test(value); } defaultMessage(args: ValidationArguments) {