diff --git a/azure-pipelines-staging-ccb.yml b/azure-pipelines-staging-ccb.yml new file mode 100644 index 0000000..f4c7e80 --- /dev/null +++ b/azure-pipelines-staging-ccb.yml @@ -0,0 +1,312 @@ +# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと +# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと +trigger: + branches: + include: + - release-ccb + tags: + include: + - stage-* + +jobs: +- job: initialize + displayName: Initialize + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + persistCredentials: true + - script: | + git fetch origin release-ccb:release-ccb + if git merge-base --is-ancestor $(Build.SourceVersion) release-ccb; then + echo "This commit is in the release-ccb branch." + else + echo "This commit is not in the release-ccb branch." + exit 1 + fi + displayName: 'タグが付けられたCommitがrelease-ccbブランチに存在するか確認' +- job: backend_test + dependsOn: initialize + condition: succeeded('initialize') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_server/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_server sudo npm ci + docker-compose exec -T dictation_server sudo npm run migrate:up:test + docker-compose exec -T dictation_server sudo npm run test +- job: backend_build + dependsOn: backend_test + condition: succeeded('backend_test') + displayName: Build And Push Backend Image + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_server + verbose: false + - task: Docker@0 + displayName: build + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + dockerFile: DockerfileServerDictation.dockerfile + imageName: odmscloud/staging/dictation:$(Build.SourceVersion) + buildArguments: | + BUILD_VERSION=$(Build.SourceVersion) + - task: Docker@0 + displayName: push + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + action: Push an image + imageName: odmscloud/staging/dictation:$(Build.SourceVersion) +- job: frontend_build_staging + dependsOn: backend_build + condition: succeeded('backend_build') + displayName: Build Frontend Files(staging) + variables: + storageAccountName: saomdspipeline + environment: staging + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_client + verbose: false + - task: Bash@3 + displayName: Bash Script + inputs: + targetType: inline + script: cd dictation_client && npm run build:stg + - task: ArchiveFiles@2 + inputs: + rootFolderOrFile: dictation_client/build + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip' + replaceExistingArchive: true + - task: AzureCLI@2 + inputs: + azureSubscription: 'omds-service-connection-stg' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az storage blob upload \ + --auth-mode login \ + --account-name $(storageAccountName) \ + --container-name $(environment) \ + --name $(Build.SourceVersion).zip \ + --type block \ + --overwrite \ + --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip +- job: function_test + dependsOn: frontend_build_staging + condition: succeeded('frontend_build_staging') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_function/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_function sudo npm ci + docker-compose exec -T dictation_function sudo npm run test +- job: function_build + dependsOn: function_test + condition: succeeded('function_test') + displayName: Build And Push Function Image + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Npm@1 + displayName: npm ci + inputs: + command: ci + workingDir: dictation_function + verbose: false + - task: Docker@0 + displayName: build + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + dockerFile: DockerfileFunctionDictation.dockerfile + imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion) + buildArguments: | + BUILD_VERSION=$(Build.SourceVersion) + - task: Docker@0 + displayName: push + inputs: + azureSubscriptionEndpoint: 'omds-service-connection-stg' + azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}' + action: Push an image + imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion) +- job: backend_deploy + dependsOn: function_build + condition: succeeded('function_build') + displayName: Backend Deploy + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureWebAppContainer@1 + inputs: + azureSubscription: 'omds-service-connection-stg' + appName: 'app-odms-dictation-stg' + deployToSlotOrASE: true + resourceGroupName: 'stg-application-rg' + slotName: 'staging' + containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)' +- job: frontend_deploy + dependsOn: backend_deploy + condition: succeeded('backend_deploy') + displayName: Deploy Frontend Files + variables: + storageAccountName: saomdspipeline + environment: staging + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureCLI@2 + inputs: + azureSubscription: 'omds-service-connection-stg' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + az storage blob download \ + --auth-mode login \ + --account-name $(storageAccountName) \ + --container-name $(environment) \ + --name $(Build.SourceVersion).zip \ + --file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip + - task: Bash@3 + displayName: Bash Script + inputs: + targetType: inline + script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion) + - task: AzureStaticWebApp@0 + displayName: 'Static Web App: ' + inputs: + workingDirectory: '$(Build.SourcesDirectory)' + app_location: '/$(Build.SourceVersion)' + config_file_location: /dictation_client + skip_app_build: true + skip_api_build: true + is_static_export: false + verbose: false + azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN) +- job: function_deploy + dependsOn: frontend_deploy + condition: succeeded('frontend_deploy') + displayName: Function Deploy + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureFunctionAppContainer@1 + inputs: + azureSubscription: 'omds-service-connection-stg' + appName: 'func-odms-dictation-stg' + imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)' +- job: smoke_test + dependsOn: function_deploy + condition: succeeded('function_deploy') + displayName: 'smoke test' + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + # スモークテスト用にjobを確保 +- job: swap_slot + dependsOn: smoke_test + condition: succeeded('smoke_test') + displayName: 'Swap Staging and Production' + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureAppServiceManage@0 + displayName: 'Azure App Service Manage: app-odms-dictation-stg' + inputs: + azureSubscription: 'omds-service-connection-stg' + action: 'Swap Slots' + WebAppName: 'app-odms-dictation-stg' + ResourceGroupName: 'stg-application-rg' + SourceSlot: 'staging' + SwapWithProduction: true +- job: migration + dependsOn: swap_slot + condition: succeeded('swap_slot') + displayName: DB migration + pool: + name: odms-deploy-pipeline + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: AzureKeyVault@2 + displayName: 'Azure Key Vault: kv-odms-secret-stg' + inputs: + ConnectedServiceName: 'omds-service-connection-stg' + KeyVaultName: kv-odms-secret-stg + - task: CmdLine@2 + displayName: migration + inputs: + script: >2 + # DB接続情報書き換え + sed -i -e "s/DB_NAME_CCB/$(db-name-ccb)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml + sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml + sql-migrate --version + cat ./dictation_server/db/dbconfig.yml + # migration実行 + sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci_ccb \ No newline at end of file diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index 918dcf9..042a731 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -170,9 +170,29 @@ jobs: --type block \ --overwrite \ --file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip -- job: function_build +- job: function_test dependsOn: frontend_build_production condition: succeeded('frontend_build_production') + displayName: UnitTest + pool: + vmImage: ubuntu-latest + steps: + - checkout: self + clean: true + fetchDepth: 1 + - task: Bash@3 + displayName: Bash Script (Test) + inputs: + targetType: inline + workingDirectory: dictation_function/.devcontainer + script: | + docker-compose -f pipeline-docker-compose.yml build + docker-compose -f pipeline-docker-compose.yml up -d + docker-compose exec -T dictation_function sudo npm ci + docker-compose exec -T dictation_function sudo npm run test +- job: function_build + dependsOn: function_test + condition: succeeded('function_test') displayName: Build And Push Function Image pool: name: odms-deploy-pipeline @@ -186,32 +206,6 @@ jobs: command: ci workingDir: dictation_function verbose: false - - task: AzureKeyVault@2 - displayName: 'Azure Key Vault: kv-odms-secret-stg' - inputs: - ConnectedServiceName: 'omds-service-connection-stg' - KeyVaultName: kv-odms-secret-stg - SecretsFilter: '*' - - task: Bash@3 - displayName: Bash Script (Test) - inputs: - targetType: inline - script: | - cd dictation_function - npm run test - env: - TENANT_NAME: xxxxxxxxxxxx - SIGNIN_FLOW_NAME: xxxxxxxxxxxx - ADB2C_TENANT_ID: $(adb2c-tenant-id) - ADB2C_CLIENT_ID: $(adb2c-client-id) - ADB2C_CLIENT_SECRET: $(adb2c-client-secret) - ADB2C_ORIGIN: xxxxxx - SENDGRID_API_KEY: $(sendgrid-api-key) - MAIL_FROM: xxxxxx - APP_DOMAIN: http://localhost:8081/ - REDIS_HOST: xxxxxxxxxxxx - REDIS_PORT: 0 - REDIS_PASSWORD: xxxxxxxxxxxx - task: Docker@0 displayName: build inputs: diff --git a/dictation_client/jest.config.js b/dictation_client/jest.config.js new file mode 100644 index 0000000..b413e10 --- /dev/null +++ b/dictation_client/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; \ No newline at end of file diff --git a/dictation_client/package-lock.json b/dictation_client/package-lock.json index 05a0af5..8e5f96d 100644 --- a/dictation_client/package-lock.json +++ b/dictation_client/package-lock.json @@ -15,7 +15,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.2.1", - "@types/jest": "^27.5.2", "@types/node": "^17.0.45", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.6", @@ -28,6 +27,7 @@ "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "luxon": "^3.3.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-google-recaptcha-v3": "^1.10.0", @@ -46,8 +46,10 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@mdx-js/react": "^2.1.2", "@openapitools/openapi-generator-cli": "^2.5.2", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/luxon": "^3.2.0", + "@types/papaparse": "^5.3.14", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.3", @@ -57,16 +59,18 @@ "babel-loader": "^8.2.5", "eslint": "^8.19.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "redux-mock-store": "^1.5.4", "sass": "^1.58.3", + "ts-jest": "^29.1.2", "typescript": "^4.7.4", "vite": "^4.1.4", "vite-plugin-env-compatible": "^1.1.1", @@ -108,11 +112,6 @@ "node": ">=12.0.0" } }, - "node_modules/@azure/abort-controller/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-auth": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.5.0.tgz", @@ -126,11 +125,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-http": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.3.tgz", @@ -155,11 +149,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-http/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-lro": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz", @@ -174,11 +163,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-lro/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-paging": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", @@ -190,11 +174,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-paging/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-tracing": { "version": "1.0.0-preview.13", "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", @@ -207,11 +186,6 @@ "node": ">=12.0.0" } }, - "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/core-util": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.4.0.tgz", @@ -224,11 +198,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/logger": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", @@ -240,11 +209,6 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@azure/msal-browser": { "version": "2.33.0", "license": "MIT", @@ -291,52 +255,104 @@ "node": ">=14.0.0" } }, - "node_modules/@azure/storage-blob/node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -347,12 +363,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -387,22 +403,19 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { @@ -421,22 +434,22 @@ "dev": true }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -455,40 +468,41 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.18.6", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -506,9 +520,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { "@babel/types": "^7.22.5" @@ -518,52 +532,52 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -627,9 +641,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -638,6 +652,66 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.18.6", "dev": true, @@ -652,6 +726,108 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.18.6", "dev": true, @@ -724,34 +900,34 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -759,19 +935,25 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@cush/relative": { "version": "1.0.0", "dev": true, @@ -1201,6 +1383,462 @@ "version": "1.2.1", "license": "BSD-3-Clause" }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", "dev": true, @@ -1261,12 +1899,13 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lukeed/csprng": { @@ -1294,47 +1933,27 @@ "react": ">=16" } }, - "node_modules/@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", - "dev": true, - "dependencies": { - "axios": "0.27.2" - }, - "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^6.0.0 || ^7.0.0" - } - }, "node_modules/@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", "dev": true, - "peer": true, "dependencies": { - "axios": "0.27.2", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "*", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, "class-transformer": { "optional": true }, @@ -1343,12 +1962,43 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "node_modules/@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", "dev": true, - "peer": true + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1401,28 +2051,29 @@ } }, "node_modules/@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", "dev": true, "hasInstallScript": true, "dependencies": { - "@nestjs/axios": "0.0.8", - "@nestjs/common": "9.3.11", - "@nestjs/core": "9.3.11", + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.1.6", - "inquirer": "8.2.5", + "glob": "7.2.3", + "inquirer": "8.2.6", "lodash": "4.17.21", "reflect-metadata": "0.1.13", - "rxjs": "7.8.0", - "tslib": "2.0.3" + "rxjs": "7.8.1", + "tslib": "2.6.2" }, "bin": { "openapi-generator-cli": "main.js" @@ -1435,89 +2086,29 @@ "url": "https://opencollective.com/openapi_generator" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "dev": true, - "dependencies": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, "peerDependencies": { - "cache-manager": "<=5", - "class-transformer": "*", - "class-validator": "*", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, - "class-transformer": { - "optional": true - }, - "class-validator": { - "optional": true - } + "rxjs": "^6.0.0 || ^7.0.0" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", - "integrity": "sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==", + "node_modules/@openapitools/openapi-generator-cli/node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, - "hasInstallScript": true, "dependencies": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nest" - }, - "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/websockets": "^9.0.0", - "reflect-metadata": "^0.1.12", - "rxjs": "^7.1.0" - }, - "peerDependenciesMeta": { - "@nestjs/microservices": { - "optional": true - }, - "@nestjs/platform-express": { - "optional": true - }, - "@nestjs/websockets": { - "optional": true - } + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, "node_modules/@openapitools/openapi-generator-cli/node_modules/commander": { "version": "8.3.0", "dev": true, @@ -1526,25 +2117,6 @@ "node": ">= 12" } }, - "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { - "version": "7.1.6", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@opentelemetry/api": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.6.0.tgz", @@ -1594,6 +2166,29 @@ "node": ">= 8.0.0" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@testing-library/dom": { "version": "8.14.0", "license": "MIT", @@ -1673,6 +2268,47 @@ "version": "4.2.2", "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/eslint": { "version": "8.4.10", "dev": true, @@ -1700,6 +2336,15 @@ "dev": true, "peer": true }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/history": { "version": "4.7.11", "license": "MIT" @@ -1712,12 +2357,58 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/jest": { - "version": "27.5.2", - "license": "MIT", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dependencies": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/json-schema": { @@ -1758,6 +2449,15 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.5", "license": "MIT" @@ -1807,6 +2507,11 @@ "version": "0.16.2", "license": "MIT" }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.5", "license": "MIT", @@ -1826,6 +2531,19 @@ "version": "0.0.3", "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.5", "dev": true, @@ -2510,6 +3228,27 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, "node_modules/babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -2529,6 +3268,92 @@ "webpack": ">=2" } }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" @@ -2590,7 +3415,6 @@ }, "node_modules/braces": { "version": "3.0.2", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -2600,7 +3424,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -2610,14 +3436,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2626,6 +3455,27 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2653,8 +3503,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/call-bind": { "version": "1.0.2", @@ -2675,6 +3524,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "dev": true, @@ -2684,7 +3542,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001464", + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", "dev": true, "funding": [ { @@ -2694,9 +3554,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -2712,6 +3575,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -2764,6 +3636,26 @@ "node": ">=6.0" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "node_modules/classnames": { "version": "2.3.2", "license": "MIT" @@ -2788,9 +3680,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -2826,6 +3718,22 @@ "node": ">=0.8" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, "node_modules/color-convert": { "version": "2.0.1", "license": "MIT", @@ -2945,11 +3853,30 @@ } }, "node_modules/convert-source-map": { - "version": "1.8.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, - "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/cross-spawn": { @@ -3028,10 +3955,33 @@ "node": ">=0.10" } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-is": { "version": "0.1.4", "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "dev": true, @@ -3073,6 +4023,15 @@ "node": ">=6" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "dev": true, @@ -3082,13 +4041,6 @@ "wrappy": "1" } }, - "node_modules/diff-sequences": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -3123,9 +4075,22 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.328", + "version": "1.4.681", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", + "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, - "license": "ISC" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -3154,6 +4119,15 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -3388,9 +4362,10 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.5.0", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, - "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3707,6 +4682,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.4.0", "license": "BSD-3-Clause", @@ -3753,6 +4741,121 @@ "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expect/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3824,6 +4927,15 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -3851,7 +4963,6 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3904,14 +5015,15 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.1", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4039,6 +5151,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.0", "dev": true, @@ -4154,7 +5287,6 @@ }, "node_modules/graceful-fs": { "version": "4.2.10", - "dev": true, "license": "ISC" }, "node_modules/has": { @@ -4247,6 +5379,12 @@ "dev": true, "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "license": "MIT", @@ -4254,6 +5392,15 @@ "void-elements": "3.1.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/i18next": { "version": "21.10.0", "funding": [ @@ -4348,6 +5495,25 @@ "node": ">=4" } }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "license": "MIT", @@ -4375,9 +5541,9 @@ "license": "ISC" }, "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -4394,12 +5560,26 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" } }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -4428,6 +5608,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.4", "dev": true, @@ -4517,6 +5703,15 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "license": "MIT", @@ -4549,7 +5744,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4595,6 +5789,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "dev": true, @@ -4669,6 +5875,117 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -4678,37 +5995,971 @@ "node": ">=6" } }, - "node_modules/jest-diff": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/jest-get-type": { - "version": "27.5.1", - "license": "MIT", - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -4823,6 +7074,15 @@ "version": "3.1.2", "license": "MIT" }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "dev": true, @@ -4836,6 +7096,15 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "license": "MIT", @@ -4940,6 +7209,12 @@ "node": ">=4" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/loader-runner": { "version": "4.3.0", "dev": true, @@ -4983,6 +7258,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "license": "MIT" @@ -5062,12 +7343,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -5079,7 +7374,6 @@ }, "node_modules/micromatch": { "version": "4.0.5", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.2", @@ -5159,9 +7453,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -5205,10 +7499,17 @@ } } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node_modules/node-releases": { - "version": "2.0.10", - "dev": true, - "license": "MIT" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true }, "node_modules/nopt": { "version": "4.0.3", @@ -5255,6 +7556,18 @@ "dev": true, "license": "ISC" }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -5474,6 +7787,11 @@ "node": ">=4" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "license": "MIT", @@ -5484,6 +7802,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "3.0.0", "dev": true, @@ -5532,7 +7868,6 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5541,6 +7876,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -5624,9 +7968,9 @@ } }, "node_modules/postcss": { - "version": "8.4.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", - "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -5643,7 +7987,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -5677,9 +8021,10 @@ } }, "node_modules/prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -5735,6 +8080,19 @@ "node": ">= 0.6.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -5748,6 +8106,12 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/punycode": { "version": "2.1.1", "license": "MIT", @@ -5755,6 +8119,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -6109,6 +8489,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -6234,24 +8644,19 @@ } }, "node_modules/rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "dependencies": { "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", - "dev": true - }, "node_modules/safe-buffer": { "version": "5.1.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/safe-regex-test": { "version": "1.0.0", @@ -6374,9 +8779,14 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6488,6 +8898,31 @@ "spdx-ranges": "^2.0.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6517,6 +8952,19 @@ } ] }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "dev": true, @@ -6609,6 +9057,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "license": "MIT", @@ -6733,6 +9199,20 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "license": "MIT" @@ -6755,6 +9235,12 @@ "node": ">=0.6.0" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "node_modules/to-fast-properties": { "version": "2.0.0", "dev": true, @@ -6765,7 +9251,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6795,6 +9280,73 @@ "node": ">=0.6" } }, + "node_modules/ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "dev": true, @@ -6826,9 +9378,9 @@ } }, "node_modules/tslib": { - "version": "2.0.3", - "dev": true, - "license": "0BSD" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -6867,6 +9419,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "license": "(MIT OR CC0-1.0)", @@ -6904,9 +9465,9 @@ } }, "node_modules/uid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz", - "integrity": "sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "dev": true, "dependencies": { "@lukeed/csprng": "^1.0.0" @@ -6939,7 +9500,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -6949,15 +9512,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -7000,6 +9566,20 @@ "version": "2.3.0", "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "dev": true, @@ -7010,9 +9590,9 @@ } }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -7125,6 +9705,15 @@ "node": ">=0.10.0" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.4.0", "dev": true, @@ -7308,6 +9897,19 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -7365,6 +9967,18 @@ "engines": { "node": ">=10" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { @@ -7387,13 +10001,6 @@ "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-auth": { @@ -7404,13 +10011,6 @@ "@azure/abort-controller": "^1.0.0", "@azure/core-util": "^1.1.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-http": { @@ -7432,13 +10032,6 @@ "tunnel": "^0.0.6", "uuid": "^8.3.0", "xml2js": "^0.5.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-lro": { @@ -7450,13 +10043,6 @@ "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-paging": { @@ -7465,13 +10051,6 @@ "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-tracing": { @@ -7481,13 +10060,6 @@ "requires": { "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/core-util": { @@ -7497,13 +10069,6 @@ "requires": { "@azure/abort-controller": "^1.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/logger": { @@ -7512,13 +10077,6 @@ "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", "requires": { "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@azure/msal-browser": { @@ -7547,59 +10105,99 @@ "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - } } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true }, "@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" } }, "@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -7626,16 +10224,16 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", - "semver": "^6.3.0" + "semver": "^6.3.1" }, "dependencies": { "lru-cache": { @@ -7656,19 +10254,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -7681,32 +10279,31 @@ } }, "@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" } }, "@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" } }, "@babel/helper-plugin-utils": { - "version": "7.18.6", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, "@babel/helper-simple-access": { @@ -7719,49 +10316,49 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true }, "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -7812,11 +10409,56 @@ } }, "@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, "@babel/plugin-syntax-jsx": { "version": "7.18.6", "dev": true, @@ -7824,6 +10466,78 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, "@babel/plugin-transform-react-jsx": { "version": "7.18.6", "dev": true, @@ -7865,45 +10579,51 @@ } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" } }, "@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@cush/relative": { "version": "1.0.0", "dev": true @@ -8113,6 +10833,361 @@ "@humanwhocodes/object-schema": { "version": "1.2.1" }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "requires": { + "jest-get-type": "^29.6.3" + }, + "dependencies": { + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + } + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@jridgewell/gen-mapping": { "version": "0.1.1", "dev": true, @@ -8159,11 +11234,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.17", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz", + "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@lukeed/csprng": { @@ -8180,35 +11257,29 @@ "@types/react": ">=16" } }, - "@nestjs/axios": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.0.8.tgz", - "integrity": "sha512-oJyfR9/h9tVk776il0829xyj3b2e81yTu6HjPraxynwNtMNGqZBHHmAQL24yMB3tVbBM0RvG3eUXH8+pRCGwlg==", + "@nestjs/common": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", "dev": true, "requires": { - "axios": "0.27.2" + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" } }, - "@nestjs/common": { - "version": "8.4.7", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-8.4.7.tgz", - "integrity": "sha512-m/YsbcBal+gA5CFrDpqXqsSfylo+DIQrkFY3qhVIltsYRfu8ct8J9pqsTO6OPf3mvqdOpFGrV5sBjoyAzOBvsw==", + "@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", "dev": true, - "peer": true, "requires": { - "axios": "0.27.2", + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "tslib": "2.4.0", - "uuid": "8.3.2" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true, - "peer": true - } + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" } }, "@nodelib/fs.scandir": { @@ -8243,85 +11314,51 @@ } }, "@openapitools/openapi-generator-cli": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.6.0.tgz", - "integrity": "sha512-M/aOpR7G+Y1nMf+ofuar8pGszajgfhs1aSPSijkcr2tHTxKAI3sA3YYcOGbszxaNRKFyvOcDq+KP9pcJvKoCHg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", "dev": true, "requires": { - "@nestjs/axios": "0.0.8", - "@nestjs/common": "9.3.11", - "@nestjs/core": "9.3.11", + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", "chalk": "4.1.2", "commander": "8.3.0", "compare-versions": "4.1.4", "concurrently": "6.5.1", "console.table": "0.10.0", "fs-extra": "10.1.0", - "glob": "7.1.6", - "inquirer": "8.2.5", + "glob": "7.2.3", + "inquirer": "8.2.6", "lodash": "4.17.21", "reflect-metadata": "0.1.13", - "rxjs": "7.8.0", - "tslib": "2.0.3" + "rxjs": "7.8.1", + "tslib": "2.6.2" }, "dependencies": { - "@nestjs/common": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.3.11.tgz", - "integrity": "sha512-IFZ2G/5UKWC2Uo7tJ4SxGed2+aiA+sJyWeWsGTogKVDhq90oxVBToh+uCDeI31HNUpqYGoWmkletfty42zUd8A==", + "@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "dev": true, - "requires": { - "iterare": "1.2.1", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - } - } + "requires": {} }, - "@nestjs/core": { - "version": "9.3.11", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.3.11.tgz", - "integrity": "sha512-CI27a2JFd5rvvbgkalWqsiwQNhcP4EAG5BUK8usjp29wVp1kx30ghfBT8FLqIgmkRVo65A0IcEnWsxeXMntkxQ==", + "axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dev": true, "requires": { - "@nuxtjs/opencollective": "0.3.2", - "fast-safe-stringify": "2.1.1", - "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.5.0", - "uid": "2.0.1" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - } + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "commander": { "version": "8.3.0", "dev": true - }, - "glob": { - "version": "7.1.6", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } } } }, @@ -8350,6 +11387,29 @@ "picomatch": "^2.2.2" } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, "@testing-library/dom": { "version": "8.14.0", "requires": { @@ -8401,6 +11461,47 @@ "@types/aria-query": { "version": "4.2.2" }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, "@types/eslint": { "version": "8.4.10", "dev": true, @@ -8426,6 +11527,15 @@ "dev": true, "peer": true }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/history": { "version": "4.7.11" }, @@ -8436,11 +11546,51 @@ "hoist-non-react-statics": "^3.3.0" } }, - "@types/jest": { - "version": "27.5.2", + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "requires": { - "jest-matcher-utils": "^27.0.0", - "pretty-format": "^27.0.0" + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } } }, "@types/json-schema": { @@ -8475,6 +11625,15 @@ "form-data": "^4.0.0" } }, + "@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/prop-types": { "version": "15.7.5" }, @@ -8517,6 +11676,11 @@ "@types/scheduler": { "version": "0.16.2" }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, "@types/testing-library__jest-dom": { "version": "5.14.5", "requires": { @@ -8534,6 +11698,19 @@ "@types/use-sync-external-store": { "version": "0.0.3" }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "@typescript-eslint/eslint-plugin": { "version": "5.30.5", "dev": true, @@ -9014,6 +12191,21 @@ "dequal": "^2.0.3" } }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, "babel-loader": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", @@ -9026,6 +12218,76 @@ "schema-utils": "^2.6.5" } }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "balanced-match": { "version": "1.0.2" }, @@ -9063,19 +12325,38 @@ }, "braces": { "version": "3.0.2", - "dev": true, "requires": { "fill-range": "^7.0.1" } }, "browserslist": { - "version": "4.21.5", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" } }, "buffer": { @@ -9090,8 +12371,7 @@ }, "buffer-from": { "version": "1.1.2", - "dev": true, - "peer": true + "dev": true }, "call-bind": { "version": "1.0.2", @@ -9104,12 +12384,20 @@ "callsites": { "version": "3.1.0" }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "camelcase-css": { "version": "2.0.1", "dev": true }, "caniuse-lite": { - "version": "1.0.30001464", + "version": "1.0.30001589", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz", + "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==", "dev": true }, "chalk": { @@ -9119,6 +12407,12 @@ "supports-color": "^7.1.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -9153,6 +12447,17 @@ "dev": true, "peer": true }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + }, + "cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, "classnames": { "version": "2.3.2" }, @@ -9172,9 +12477,9 @@ } }, "cli-spinners": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", - "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true }, "cli-width": { @@ -9196,6 +12501,18 @@ "version": "1.0.4", "dev": true }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, "color-convert": { "version": "2.0.1", "requires": { @@ -9281,10 +12598,24 @@ } }, "convert-source-map": { - "version": "1.8.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, "requires": { - "safe-buffer": "~5.1.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" } }, "cross-spawn": { @@ -9330,9 +12661,22 @@ "decode-uri-component": { "version": "0.2.2" }, + "dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "requires": {} + }, "deep-is": { "version": "0.1.4" }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "defaults": { "version": "1.0.4", "dev": true, @@ -9356,6 +12700,12 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, "dezalgo": { "version": "1.0.4", "dev": true, @@ -9364,9 +12714,6 @@ "wrappy": "1" } }, - "diff-sequences": { - "version": "27.5.1" - }, "dir-glob": { "version": "3.0.1", "dev": true, @@ -9391,7 +12738,15 @@ } }, "electron-to-chromium": { - "version": "1.4.328", + "version": "1.4.681", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz", + "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true }, "emoji-regex": { @@ -9413,6 +12768,15 @@ "tapable": "^2.2.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.21.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", @@ -9604,7 +12968,9 @@ } }, "eslint-config-prettier": { - "version": "8.5.0", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, "requires": {} }, @@ -9802,6 +13168,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "esquery": { "version": "1.4.0", "requires": { @@ -9827,6 +13199,90 @@ "events": { "version": "3.3.0" }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -9884,6 +13340,15 @@ "reusify": "^1.0.4" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -9901,7 +13366,6 @@ }, "fill-range": { "version": "7.0.1", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -9935,7 +13399,9 @@ "version": "3.2.6" }, "follow-redirects": { - "version": "1.15.1" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "for-each": { "version": "0.3.3", @@ -10016,6 +13482,18 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "get-symbol-description": { "version": "1.0.0", "dev": true, @@ -10091,8 +13569,7 @@ } }, "graceful-fs": { - "version": "4.2.10", - "dev": true + "version": "4.2.10" }, "has": { "version": "1.0.3", @@ -10147,12 +13624,24 @@ "version": "2.8.9", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-parse-stringify": { "version": "3.0.1", "requires": { "void-elements": "3.1.0" } }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "i18next": { "version": "21.10.0", "requires": { @@ -10196,6 +13685,16 @@ } } }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4" }, @@ -10213,9 +13712,9 @@ "version": "2.0.4" }, "inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -10232,7 +13731,20 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } } }, "internal-slot": { @@ -10257,6 +13769,12 @@ "is-typed-array": "^1.1.10" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.4", "dev": true, @@ -10306,6 +13824,12 @@ "version": "3.0.0", "dev": true }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "requires": { @@ -10323,8 +13847,7 @@ "dev": true }, "is-number": { - "version": "7.0.0", - "dev": true + "version": "7.0.0" }, "is-number-object": { "version": "1.0.7", @@ -10348,6 +13871,12 @@ "call-bind": "^1.0.2" } }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-string": { "version": "1.0.7", "dev": true, @@ -10391,31 +13920,816 @@ "isexe": { "version": "2.0.0" }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", "dev": true }, - "jest-diff": { - "version": "27.5.1", + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" } }, - "jest-get-type": { - "version": "27.5.1" + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + } + } }, - "jest-matcher-utils": { - "version": "27.5.1", + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" } }, "jest-worker": { @@ -10496,6 +14810,12 @@ "jwt-decode": { "version": "3.1.2" }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "language-subtag-registry": { "version": "0.3.22", "dev": true @@ -10507,6 +14827,12 @@ "language-subtag-registry": "~0.3.2" } }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "levn": { "version": "0.4.1", "requires": { @@ -10583,6 +14909,12 @@ } } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "loader-runner": { "version": "4.3.0", "dev": true, @@ -10612,6 +14944,12 @@ "version": "4.0.6", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "lodash.merge": { "version": "4.6.2" }, @@ -10662,12 +15000,26 @@ "semver": "^6.0.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "peer": true + "dev": true }, "merge2": { "version": "1.4.1", @@ -10675,7 +15027,6 @@ }, "micromatch": { "version": "4.0.5", - "dev": true, "requires": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -10726,9 +15077,9 @@ "dev": true }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, "natural-compare": { @@ -10747,8 +15098,16 @@ "whatwg-url": "^5.0.0" } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node-releases": { - "version": "2.0.10", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "nopt": { @@ -10785,6 +15144,15 @@ "version": "1.0.1", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "object-assign": { "version": "4.1.1" }, @@ -10932,12 +15300,29 @@ "version": "1.0.0", "dev": true }, + "papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "parent-module": { "version": "1.0.1", "requires": { "callsites": "^3.0.0" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "3.0.0", "dev": true @@ -10967,7 +15352,12 @@ "dev": true }, "picomatch": { - "version": "2.3.1", + "version": "2.3.1" + }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true }, "pkg-dir": { @@ -11031,12 +15421,12 @@ } }, "postcss": { - "version": "8.4.30", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.30.tgz", - "integrity": "sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } @@ -11052,7 +15442,9 @@ "version": "1.2.1" }, "prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, "prettier-linter-helpers": { @@ -11083,6 +15475,16 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "prop-types": { "version": "15.8.1", "requires": { @@ -11096,9 +15498,21 @@ } } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "punycode": { "version": "2.1.1" }, + "pure-rand": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", + "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "dev": true @@ -11314,6 +15728,27 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -11402,25 +15837,18 @@ } }, "rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, "requires": { "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", - "dev": true - } } }, "safe-buffer": { "version": "5.1.2", - "dev": true + "dev": true, + "peer": true }, "safe-regex-test": { "version": "1.0.0", @@ -11510,10 +15938,15 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "slash": { - "version": "3.0.0", + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "slash": { + "version": "3.0.0" + }, "slide": { "version": "1.1.6", "dev": true @@ -11597,6 +16030,27 @@ "spdx-ranges": "^2.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -11614,6 +16068,16 @@ } } }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "dev": true, @@ -11684,6 +16148,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-indent": { "version": "3.0.0", "requires": { @@ -11751,6 +16227,17 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "text-table": { "version": "0.2.0" }, @@ -11769,13 +16256,18 @@ "os-tmpdir": "~1.0.2" } }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "dev": true }, "to-regex-range": { "version": "5.0.1", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -11793,6 +16285,39 @@ "version": "1.1.0", "dev": true }, + "ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "tsconfig-paths": { "version": "3.14.1", "dev": true, @@ -11817,8 +16342,9 @@ } }, "tslib": { - "version": "2.0.3", - "dev": true + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", @@ -11844,6 +16370,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2" }, @@ -11863,9 +16395,9 @@ "dev": true }, "uid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.1.tgz", - "integrity": "sha512-PF+1AnZgycpAIEmNtjxGBVmKbZAQguaa4pBUq6KNaGEcpzZ2klCNZLM34tsjp76maN00TttiiUf6zkIBpJQm2A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "dev": true, "requires": { "@lukeed/csprng": "^1.0.0" @@ -11888,7 +16420,9 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.10", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -11923,6 +16457,17 @@ "v8-compile-cache": { "version": "2.3.0" }, + "v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "validate-npm-package-license": { "version": "3.0.4", "dev": true, @@ -11932,9 +16477,9 @@ } }, "vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "requires": { "esbuild": "^0.18.10", @@ -11982,6 +16527,15 @@ "void-elements": { "version": "3.1.0" }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "watchpack": { "version": "2.4.0", "dev": true, @@ -12110,6 +16664,16 @@ "wrappy": { "version": "1.0.2" }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, "xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", @@ -12148,6 +16712,12 @@ "yargs-parser": { "version": "20.2.9", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/dictation_client/package.json b/dictation_client/package.json index ac8d5c0..feb1f28 100644 --- a/dictation_client/package.json +++ b/dictation_client/package.json @@ -15,7 +15,8 @@ "typecheck": "tsc --noEmit", "codegen": "sh codegen.sh", "lint": "eslint --cache . --ext .js,.ts,.tsx", - "lint:fix": "npm run lint -- --fix" + "lint:fix": "npm run lint -- --fix", + "test": "jest" }, "dependencies": { "@azure/msal-browser": "^2.33.0", @@ -25,7 +26,6 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.2.1", - "@types/jest": "^27.5.2", "@types/node": "^17.0.45", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.6", @@ -38,6 +38,7 @@ "jwt-decode": "^3.1.2", "lodash": "^4.17.21", "luxon": "^3.3.0", + "papaparse": "^5.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-google-recaptcha-v3": "^1.10.0", @@ -56,8 +57,10 @@ "@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@mdx-js/react": "^2.1.2", "@openapitools/openapi-generator-cli": "^2.5.2", + "@types/jest": "^29.5.12", "@types/lodash": "^4.14.191", "@types/luxon": "^3.2.0", + "@types/papaparse": "^5.3.14", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@types/redux-mock-store": "^1.0.3", @@ -67,16 +70,18 @@ "babel-loader": "^8.2.5", "eslint": "^8.19.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", + "jest": "^29.7.0", "license-checker": "^25.0.1", - "prettier": "^2.7.1", + "prettier": "^2.8.8", "redux-mock-store": "^1.5.4", "sass": "^1.58.3", + "ts-jest": "^29.1.2", "typescript": "^4.7.4", "vite": "^4.1.4", "vite-plugin-env-compatible": "^1.1.1", @@ -99,4 +104,4 @@ } ] } -} \ No newline at end of file +} diff --git a/dictation_client/src/api/api.ts b/dictation_client/src/api/api.ts index 645f058..72982f3 100644 --- a/dictation_client/src/api/api.ts +++ b/dictation_client/src/api/api.ts @@ -78,6 +78,18 @@ export interface Account { * @memberof Account */ 'delegationPermission': boolean; + /** + * + * @type {boolean} + * @memberof Account + */ + 'autoFileDelete': boolean; + /** + * + * @type {number} + * @memberof Account + */ + 'fileRetentionDays': number; /** * * @type {number} @@ -681,6 +693,19 @@ export interface DeleteAccountRequest { */ 'accountId': number; } +/** + * + * @export + * @interface DeletePartnerAccountRequest + */ +export interface DeletePartnerAccountRequest { + /** + * 削除対象のアカウントID + * @type {number} + * @memberof DeletePartnerAccountRequest + */ + 'targetAccountId': number; +} /** * * @export @@ -700,6 +725,25 @@ export interface ErrorResponse { */ 'code': string; } +/** + * + * @export + * @interface FileRenameRequest + */ +export interface FileRenameRequest { + /** + * ファイル名変更対象の音声ファイルID + * @type {number} + * @memberof FileRenameRequest + */ + 'audioFileId': number; + /** + * 変更するファイル名 + * @type {string} + * @memberof FileRenameRequest + */ + 'fileName': string; +} /** * * @export @@ -1010,6 +1054,32 @@ export interface GetPartnerLicensesResponse { */ 'childrenPartnerLicenses': Array; } +/** + * + * @export + * @interface GetPartnerUsersRequest + */ +export interface GetPartnerUsersRequest { + /** + * 取得対象のアカウントID + * @type {number} + * @memberof GetPartnerUsersRequest + */ + 'targetAccountId': number; +} +/** + * + * @export + * @interface GetPartnerUsersResponse + */ +export interface GetPartnerUsersResponse { + /** + * + * @type {Array} + * @memberof GetPartnerUsersResponse + */ + 'users': Array; +} /** * * @export @@ -1332,6 +1402,92 @@ export interface LicenseOrder { */ 'status': string; } +/** + * + * @export + * @interface MultipleImportErrors + */ +export interface MultipleImportErrors { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportErrors + */ + 'name': string; + /** + * エラー発生行数 + * @type {number} + * @memberof MultipleImportErrors + */ + 'line': number; + /** + * エラーコード + * @type {string} + * @memberof MultipleImportErrors + */ + 'errorCode': string; +} +/** + * + * @export + * @interface MultipleImportUser + */ +export interface MultipleImportUser { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof MultipleImportUser + */ + 'email': string; + /** + * 0(none)/1(author)/2(typist) + * @type {number} + * @memberof MultipleImportUser + */ + 'role': number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'authorId'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'autoRenew': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'notification': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'encryption'?: number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'encryptionPassword'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'prompt'?: number; +} /** * * @export @@ -1474,6 +1630,37 @@ export interface PartnerLicenseInfo { */ 'issueRequesting': number; } +/** + * + * @export + * @interface PartnerUser + */ +export interface PartnerUser { + /** + * ユーザーID + * @type {number} + * @memberof PartnerUser + */ + 'id': number; + /** + * ユーザー名 + * @type {string} + * @memberof PartnerUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof PartnerUser + */ + 'email': string; + /** + * プライマリ管理者かどうか + * @type {boolean} + * @memberof PartnerUser + */ + 'isPrimaryAdmin': boolean; +} /** * * @export @@ -1500,6 +1687,69 @@ export interface PostCheckoutPermissionRequest { */ 'assignees': Array; } +/** + * + * @export + * @interface PostDeleteUserRequest + */ +export interface PostDeleteUserRequest { + /** + * 削除対象のユーザーID + * @type {number} + * @memberof PostDeleteUserRequest + */ + 'userId': number; +} +/** + * + * @export + * @interface PostMultipleImportsCompleteRequest + */ +export interface PostMultipleImportsCompleteRequest { + /** + * アカウントID + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'accountId': number; + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsCompleteRequest + */ + 'filename': string; + /** + * 一括登録受付時刻(UNIXTIME/ミリ秒) + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'requestTime': number; + /** + * + * @type {Array} + * @memberof PostMultipleImportsCompleteRequest + */ + 'errors': Array; +} +/** + * + * @export + * @interface PostMultipleImportsRequest + */ +export interface PostMultipleImportsRequest { + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsRequest + */ + 'filename': string; + /** + * + * @type {Array} + * @memberof PostMultipleImportsRequest + */ + 'users': Array; +} /** * * @export @@ -1679,6 +1929,25 @@ export interface SignupRequest { */ 'prompt'?: boolean; } +/** + * + * @export + * @interface SwitchParentRequest + */ +export interface SwitchParentRequest { + /** + * 切り替え先の親アカウントID + * @type {number} + * @memberof SwitchParentRequest + */ + 'to': number; + /** + * 親を変更したいアカウントIDのリスト + * @type {Array} + * @memberof SwitchParentRequest + */ + 'children': Array; +} /** * * @export @@ -1721,6 +1990,12 @@ export interface Task { * @memberof Task */ 'fileName': string; + /** + * 生(Blob Storage上の)音声ファイル名 + * @type {string} + * @memberof Task + */ + 'rawFileName': string; /** * 音声ファイルの録音時間(ミリ秒の整数値) * @type {string} @@ -2064,6 +2339,25 @@ export interface UpdateAccountInfoRequest { */ 'secondryAdminUserId'?: number; } +/** + * + * @export + * @interface UpdateFileDeleteSettingRequest + */ +export interface UpdateFileDeleteSettingRequest { + /** + * 自動ファイル削除をするかどうか + * @type {boolean} + * @memberof UpdateFileDeleteSettingRequest + */ + 'autoFileDelete': boolean; + /** + * 文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間 + * @type {number} + * @memberof UpdateFileDeleteSettingRequest + */ + 'retentionDays': number; +} /** * * @export @@ -2077,6 +2371,50 @@ export interface UpdateOptionItemsRequest { */ 'optionItems': Array; } +/** + * + * @export + * @interface UpdatePartnerInfoRequest + */ +export interface UpdatePartnerInfoRequest { + /** + * 変更対象アカウントID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'targetAccountId': number; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdatePartnerInfoRequest + */ + 'primaryAdminUserId': number; + /** + * 会社名 + * @type {string} + * @memberof UpdatePartnerInfoRequest + */ + 'companyName': string; +} +/** + * + * @export + * @interface UpdateRestrictionStatusRequest + */ +export interface UpdateRestrictionStatusRequest { + /** + * 操作対象の第五階層アカウントID + * @type {number} + * @memberof UpdateRestrictionStatusRequest + */ + 'accountId': number; + /** + * 制限をかけるかどうか(trur:制限をかける) + * @type {boolean} + * @memberof UpdateRestrictionStatusRequest + */ + 'restricted': boolean; +} /** * * @export @@ -2639,6 +2977,84 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePartnerAccount: async (deletePartnerAccountRequest: DeletePartnerAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deletePartnerAccountRequest' is not null or undefined + assertParamExists('deletePartnerAccount', 'deletePartnerAccountRequest', deletePartnerAccountRequest) + const localVarPath = `/accounts/partner/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deletePartnerAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('deleteTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}/delete` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3009,6 +3425,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers: async (getPartnerUsersRequest: GetPartnerUsersRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getPartnerUsersRequest' is not null or undefined + assertParamExists('getPartnerUsers', 'getPartnerUsersRequest', getPartnerUsersRequest) + const localVarPath = `/accounts/partner/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getPartnerUsersRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3237,6 +3693,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + switchParent: async (switchParentRequest: SwitchParentRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'switchParentRequest' is not null or undefined + assertParamExists('switchParent', 'switchParentRequest', switchParentRequest) + const localVarPath = `/accounts/parent/switch`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(switchParentRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3277,6 +3773,46 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting: async (updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateFileDeleteSettingRequest' is not null or undefined + assertParamExists('updateFileDeleteSetting', 'updateFileDeleteSettingRequest', updateFileDeleteSettingRequest) + const localVarPath = `/accounts/me/file-delete-setting`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateFileDeleteSettingRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -3321,6 +3857,86 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat options: localVarRequestOptions, }; }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo: async (updatePartnerInfoRequest: UpdatePartnerInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updatePartnerInfoRequest' is not null or undefined + assertParamExists('updatePartnerInfo', 'updatePartnerInfoRequest', updatePartnerInfoRequest) + const localVarPath = `/accounts/partner/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus: async (updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateRestrictionStatusRequest' is not null or undefined + assertParamExists('updateRestrictionStatus', 'updateRestrictionStatusRequest', updateRestrictionStatusRequest) + const localVarPath = `/accounts/restriction-status`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateRestrictionStatusRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -3510,6 +4126,32 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.deleteAccountAndData']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deletePartnerAccount(deletePartnerAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deletePartnerAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3637,6 +4279,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.getPartnerLicenses']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerUsers(getPartnerUsersRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartnerUsers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3713,6 +4368,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.switchParent(switchParentRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.switchParent']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3726,6 +4394,19 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateAccountInfo']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateFileDeleteSetting(updateFileDeleteSettingRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateFileDeleteSetting']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -3740,6 +4421,32 @@ export const AccountsApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartnerInfo(updatePartnerInfoRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updatePartnerInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateRestrictionStatus(updateRestrictionStatusRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateRestrictionStatus']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -3848,6 +4555,26 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: any): AxiosPromise { + return localVarFp.deletePartnerAccount(deletePartnerAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.deleteTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -3945,6 +4672,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: any): AxiosPromise { + return localVarFp.getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4003,6 +4740,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise { return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + switchParent(switchParentRequest: SwitchParentRequest, options?: any): AxiosPromise { + return localVarFp.switchParent(switchParentRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4013,6 +4760,16 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: any): AxiosPromise { return localVarFp.updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: any): AxiosPromise { + return localVarFp.updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -4024,6 +4781,26 @@ export const AccountsApiFactory = function (configuration?: Configuration, baseP updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); }, + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: any): AxiosPromise { + return localVarFp.updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: any): AxiosPromise { + return localVarFp.updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -4140,6 +4917,30 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {DeletePartnerAccountRequest} deletePartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deletePartnerAccount(deletePartnerAccountRequest: DeletePartnerAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deletePartnerAccount(deletePartnerAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4257,6 +5058,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用) + * @summary + * @param {GetPartnerUsersRequest} getPartnerUsersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartnerUsers(getPartnerUsersRequest: GetPartnerUsersRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartnerUsers(getPartnerUsersRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4327,6 +5140,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {SwitchParentRequest} switchParentRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public switchParent(switchParentRequest: SwitchParentRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).switchParent(switchParentRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4339,6 +5164,18 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary @@ -4352,6 +5189,30 @@ export class AccountsApi extends BaseAPI { return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * パートナーアカウントの情報を更新します + * @summary + * @param {UpdatePartnerInfoRequest} updatePartnerInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updatePartnerInfo(updatePartnerInfoRequest: UpdatePartnerInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updatePartnerInfo(updatePartnerInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します * @summary @@ -4887,6 +5748,46 @@ export const FilesApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename: async (fileRenameRequest: FileRenameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'fileRenameRequest' is not null or undefined + assertParamExists('fileRename', 'fileRenameRequest', fileRenameRequest) + const localVarPath = `/files/rename`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(fileRenameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5071,6 +5972,19 @@ export const FilesApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['FilesApi.downloadTemplateLocation']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.fileRename(fileRenameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.fileRename']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5151,6 +6065,16 @@ export const FilesApiFactory = function (configuration?: Configuration, basePath downloadTemplateLocation(audioFileId: number, options?: any): AxiosPromise { return localVarFp.downloadTemplateLocation(audioFileId, options).then((request) => request(axios, basePath)); }, + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + fileRename(fileRenameRequest: FileRenameRequest, options?: any): AxiosPromise { + return localVarFp.fileRename(fileRenameRequest, options).then((request) => request(axios, basePath)); + }, /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5223,6 +6147,18 @@ export class FilesApi extends BaseAPI { return FilesApiFp(this.configuration).downloadTemplateLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); } + /** + * 音声ファイルの表示ファイル名を変更します + * @summary + * @param {FileRenameRequest} fileRenameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public fileRename(fileRenameRequest: FileRenameRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).fileRename(fileRenameRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します * @summary @@ -5984,6 +6920,44 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('deleteTask', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/delete` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -6207,6 +7181,19 @@ export const TasksApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTask(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTask(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.deleteTask']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary @@ -6311,6 +7298,16 @@ export const TasksApiFactory = function (configuration?: Configuration, basePath checkout(audioFileId: number, options?: any): AxiosPromise { return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath)); }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTask(audioFileId, options).then((request) => request(axios, basePath)); + }, /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary @@ -6416,6 +7413,18 @@ export class TasksApi extends BaseAPI { return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath)); } + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public deleteTask(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).deleteTask(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + /** * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します * @summary @@ -6465,6 +7474,44 @@ export class TasksApi extends BaseAPI { */ export const TemplatesApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile: async (templateFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateFileId' is not null or undefined + assertParamExists('deleteTemplateFile', 'templateFileId', templateFileId) + const localVarPath = `/templates/{templateFileId}/delete` + .replace(`{${"templateFileId"}}`, encodeURIComponent(String(templateFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6509,6 +7556,19 @@ export const TemplatesApiAxiosParamCreator = function (configuration?: Configura export const TemplatesApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = TemplatesApiAxiosParamCreator(configuration) return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTemplateFile(templateFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.deleteTemplateFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6531,6 +7591,16 @@ export const TemplatesApiFp = function(configuration?: Configuration) { export const TemplatesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = TemplatesApiFp(configuration) return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile(templateFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTemplateFile(templateFileId, options).then((request) => request(axios, basePath)); + }, /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6550,6 +7620,18 @@ export const TemplatesApiFactory = function (configuration?: Configuration, base * @extends {BaseAPI} */ export class TemplatesApi extends BaseAPI { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).deleteTemplateFile(templateFileId, options).then((request) => request(this.axios, this.basePath)); + } + /** * アカウント内のテンプレートファイルの一覧を取得します * @summary @@ -6823,6 +7905,46 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('deleteUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -6959,6 +8081,86 @@ export const UsersApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsRequest' is not null or undefined + assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest) + const localVarPath = `/users/multiple-imports`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined + assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest) + const localVarPath = `/users/multiple-imports/complete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary @@ -7177,6 +8379,19 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.deallocateLicense']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deleteUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7225,6 +8440,32 @@ export const UsersApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @summary @@ -7327,6 +8568,16 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: any): AxiosPromise { return localVarFp.deallocateLicense(deallocateLicenseRequest, options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.deleteUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, /** * ログインしているユーザーの情報を取得します * @summary @@ -7363,6 +8614,26 @@ export const UsersApiFactory = function (configuration?: Configuration, basePath getUsers(options?: any): AxiosPromise { return localVarFp.getUsers(options).then((request) => request(axios, basePath)); }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise { + return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise { + return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary @@ -7461,6 +8732,18 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).deallocateLicense(deallocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deleteUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * ログインしているユーザーの情報を取得します * @summary @@ -7505,6 +8788,30 @@ export class UsersApi extends BaseAPI { return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); } + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary diff --git a/dictation_client/src/assets/images/change_circle.svg b/dictation_client/src/assets/images/change_circle.svg new file mode 100644 index 0000000..bf74c41 --- /dev/null +++ b/dictation_client/src/assets/images/change_circle.svg @@ -0,0 +1,18 @@ + + + + + + diff --git a/dictation_client/src/assets/images/shuffle.svg b/dictation_client/src/assets/images/shuffle.svg new file mode 100644 index 0000000..ac127a9 --- /dev/null +++ b/dictation_client/src/assets/images/shuffle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dictation_client/src/assets/images/upload.svg b/dictation_client/src/assets/images/upload.svg new file mode 100644 index 0000000..2425a97 --- /dev/null +++ b/dictation_client/src/assets/images/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dictation_client/src/common/errors/code.ts b/dictation_client/src/common/errors/code.ts index 98d8cae..b1a366f 100644 --- a/dictation_client/src/common/errors/code.ts +++ b/dictation_client/src/common/errors/code.ts @@ -63,4 +63,26 @@ export const errorCodes = [ "E011004", // ワークタイプ使用中エラー "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー "E013002", // ワークフロー不在エラー + "E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + "E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + "E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + "E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + "E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + "E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + "E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + "E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている) + "E015001", // タイピストグループ削除済みエラー + "E015002", // タイピストグループがワークフローに紐づいているエラー + "E015003", // タイピストグループがルーティングされているエラー + "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + "E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) + "E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない) + "E017002", // 親アカウント変更不可エラー(階層関係が不正) + "E017003", // 親アカウント変更不可エラー(リージョンが同一でない) + "E018001", // パートナーアカウント削除エラー(削除条件を満たしていない) + "E019001", // パートナーアカウント取得不可エラー(階層構造が不正) + "E020001", // パートナーアカウント変更エラー(変更条件を満たしていない) + "E021001", // 音声ファイル名変更不可エラー(権限不足) + "E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在) ] as const; diff --git a/dictation_client/src/common/errors/types.ts b/dictation_client/src/common/errors/types.ts index 8cc801e..f4a42b4 100644 --- a/dictation_client/src/common/errors/types.ts +++ b/dictation_client/src/common/errors/types.ts @@ -6,4 +6,4 @@ export type ErrorObject = { statusCode?: number; }; -export type ErrorCodeType = typeof errorCodes[number]; +export type ErrorCodeType = (typeof errorCodes)[number]; diff --git a/dictation_client/src/common/parser.test.ts b/dictation_client/src/common/parser.test.ts new file mode 100644 index 0000000..d690cd3 --- /dev/null +++ b/dictation_client/src/common/parser.test.ts @@ -0,0 +1,153 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +// Jestによるparser.tsのテスト +import fs from "fs"; +import { CSVType, parseCSV } from "./parser"; + +describe("parse", () => { + it("指定形式のCSV文字列をパースできる", async () => { + const text = fs.readFileSync("src/common/test/test_001.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "HOGE", + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のヘッダでない場合、例外が送出される | author_id(値がoptionial)がない", async () => { + const text = fs.readFileSync("src/common/test/test_002.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のヘッダでない場合、例外が送出される | email(値が必須)がない", async () => { + const text = fs.readFileSync("src/common/test/test_003.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のヘッダでない場合、例外が送出される | emailがスペルミス", async () => { + const text = fs.readFileSync("src/common/test/test_004.csv", "utf-8"); + try { + await parseCSV(text); + fail("例外が発生しませんでした"); + } catch (e) { + expect(e).toEqual(new Error("Invalid CSV format")); + } + }); + it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(文字列)はnullとなる", async () => { + const text = fs.readFileSync("src/common/test/test_005.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: null, + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(数値)はnullとなる", async () => { + const text = fs.readFileSync("src/common/test/test_006.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: null, + author_id: "HOGE", + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); + it("指定形式のCSV文字列をパースできる | 余計なパラメータがあっても問題はない", async () => { + const text = fs.readFileSync("src/common/test/test_007.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "HOGE", + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd", + prompt: 0, + }, + { + name: "hoge2", + email: "sample2@example.com", + role: 1, + author_id: "HOGE2", + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "abcd2", + prompt: 0, + }, + ]; + expect(actualData.length).toBe(expectData.length); + + // 余計なパラメータ格納用に __parsed_extra: string[] というプロパティが作られてしまうので、既知のプロパティ毎に比較 + for (let i = 0; i < actualData.length; i += 1) { + const actualValue = actualData[i]; + const expectValue = expectData[i]; + expect(actualValue.author_id).toEqual(expectValue.author_id); + expect(actualValue.auto_assign).toEqual(expectValue.auto_assign); + expect(actualValue.email).toEqual(expectValue.email); + expect(actualValue.encryption).toEqual(expectValue.encryption); + expect(actualValue.encryption_password).toEqual( + expectValue.encryption_password + ); + expect(actualValue.name).toEqual(expectValue.name); + expect(actualValue.notification).toEqual(expectValue.notification); + expect(actualValue.prompt).toEqual(expectValue.prompt); + expect(actualValue.role).toEqual(expectValue.role); + } + }); + + it("author_id,encryption_passwordが数値のみの場合でも、文字列として変換できる", async () => { + const text = fs.readFileSync("src/common/test/test_008.csv", "utf-8"); + const actualData = await parseCSV(text); + const expectData: CSVType[] = [ + { + name: "hoge", + email: "sample@example.com", + role: 1, + author_id: "1111", + auto_assign: 1, + notification: 1, + encryption: 1, + encryption_password: "222222", + prompt: 0, + }, + ]; + expect(actualData).toEqual(expectData); + }); +}); diff --git a/dictation_client/src/common/parser.ts b/dictation_client/src/common/parser.ts new file mode 100644 index 0000000..8d76b30 --- /dev/null +++ b/dictation_client/src/common/parser.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import Papa, { ParseResult } from "papaparse"; + +export type CSVType = { + name: string | null; + email: string | null; + role: number | null; + author_id: string | null; + auto_assign: number | null; + notification: number; + encryption: number | null; + encryption_password: string | null; + prompt: number | null; +}; + +// CSVTypeのプロパティ名を文字列の配列で定義する +const CSVTypeFields: (keyof CSVType)[] = [ + "name", + "email", + "role", + "author_id", + "auto_assign", + "notification", + "encryption", + "encryption_password", + "prompt", +]; + +// 2つの配列が等しいかどうかを判定する +const equals = (lhs: string[], rhs: string[]) => { + if (lhs.length !== rhs.length) return false; + for (let i = 0; i < lhs.length; i += 1) { + if (lhs[i] !== rhs[i]) return false; + } + return true; +}; + +/** CSVファイルをCSVType型に変換するパーサー */ +export const parseCSV = async (csvString: string): Promise => + new Promise((resolve, reject) => { + Papa.parse(csvString, { + download: false, + worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定 + header: true, + dynamicTyping: { + // author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない + role: true, + auto_assign: true, + notification: true, + encryption: true, + prompt: true, + }, + // dynamicTypingがfalseの場合、空文字をnullに変換できないためtransformを使用する + transform: (value, field) => { + if (field === "author_id" || field === "encryption_password") { + // 空文字の場合はnullに変換する + if (value === "") { + return null; + } + } + return value; + }, + complete: (results: ParseResult) => { + // ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す + if (!equals(results.meta.fields ?? [], CSVTypeFields)) { + reject(new Error("Invalid CSV format")); + } + resolve(results.data); + }, + error: (error: Error) => { + reject(error); + }, + }); + }); diff --git a/dictation_client/src/common/test/test_001.csv b/dictation_client/src/common/test/test_001.csv new file mode 100644 index 0000000..4b48912 --- /dev/null +++ b/dictation_client/src/common/test/test_001.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_002.csv b/dictation_client/src/common/test/test_002.csv new file mode 100644 index 0000000..31409e4 --- /dev/null +++ b/dictation_client/src/common/test/test_002.csv @@ -0,0 +1,2 @@ +name,email,role,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_003.csv b/dictation_client/src/common/test/test_003.csv new file mode 100644 index 0000000..443c07f --- /dev/null +++ b/dictation_client/src/common/test/test_003.csv @@ -0,0 +1,2 @@ +name,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_004.csv b/dictation_client/src/common/test/test_004.csv new file mode 100644 index 0000000..a35bfa1 --- /dev/null +++ b/dictation_client/src/common/test/test_004.csv @@ -0,0 +1,2 @@ +name,emeil,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_005.csv b/dictation_client/src/common/test/test_005.csv new file mode 100644 index 0000000..4e9f9c7 --- /dev/null +++ b/dictation_client/src/common/test/test_005.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,,1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_006.csv b/dictation_client/src/common/test/test_006.csv new file mode 100644 index 0000000..93e65f3 --- /dev/null +++ b/dictation_client/src/common/test/test_006.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,,"HOGE",1,1,1,abcd,0 \ No newline at end of file diff --git a/dictation_client/src/common/test/test_007.csv b/dictation_client/src/common/test/test_007.csv new file mode 100644 index 0000000..9447f41 --- /dev/null +++ b/dictation_client/src/common/test/test_007.csv @@ -0,0 +1,3 @@ +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x +hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa \ No newline at end of file diff --git a/dictation_client/src/common/test/test_008.csv b/dictation_client/src/common/test/test_008.csv new file mode 100644 index 0000000..a40daec --- /dev/null +++ b/dictation_client/src/common/test/test_008.csv @@ -0,0 +1,2 @@ +name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt +hoge,sample@example.com,1,1111,1,1,1,222222,0 \ No newline at end of file diff --git a/dictation_client/src/features/account/accountSlice.ts b/dictation_client/src/features/account/accountSlice.ts index 48be409..ff4cd33 100644 --- a/dictation_client/src/features/account/accountSlice.ts +++ b/dictation_client/src/features/account/accountSlice.ts @@ -4,6 +4,7 @@ import { updateAccountInfoAsync, getAccountRelationsAsync, deleteAccountAsync, + updateFileDeleteSettingAsync, } from "./operations"; const initialState: AccountState = { @@ -15,6 +16,8 @@ const initialState: AccountState = { tier: 0, country: "", delegationPermission: false, + autoFileDelete: false, + fileRetentionDays: 0, }, }, dealers: [], @@ -29,6 +32,8 @@ const initialState: AccountState = { secondryAdminUserId: undefined, }, isLoading: false, + autoFileDelete: false, + fileRetentionDays: 0, }, }; @@ -64,6 +69,20 @@ export const accountSlice = createSlice({ const { secondryAdminUserId } = action.payload; state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId; }, + changeAutoFileDelete: ( + state, + action: PayloadAction<{ autoFileDelete: boolean }> + ) => { + const { autoFileDelete } = action.payload; + state.apps.autoFileDelete = autoFileDelete; + }, + changeFileRetentionDays: ( + state, + action: PayloadAction<{ fileRetentionDays: number }> + ) => { + const { fileRetentionDays } = action.payload; + state.apps.fileRetentionDays = fileRetentionDays; + }, cleanupApps: (state) => { state.domain = initialState.domain; }, @@ -85,6 +104,10 @@ export const accountSlice = createSlice({ action.payload.accountInfo.account.primaryAdminUserId; state.apps.updateAccountInfo.secondryAdminUserId = action.payload.accountInfo.account.secondryAdminUserId; + state.apps.autoFileDelete = + action.payload.accountInfo.account.autoFileDelete; + state.apps.fileRetentionDays = + action.payload.accountInfo.account.fileRetentionDays; state.apps.isLoading = false; }); builder.addCase(getAccountRelationsAsync.rejected, (state) => { @@ -99,6 +122,15 @@ export const accountSlice = createSlice({ builder.addCase(updateAccountInfoAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(updateFileDeleteSettingAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateFileDeleteSettingAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateFileDeleteSettingAsync.rejected, (state) => { + state.apps.isLoading = false; + }); builder.addCase(deleteAccountAsync.pending, (state) => { state.apps.isLoading = true; }); @@ -115,6 +147,8 @@ export const { changeDealerPermission, changePrimaryAdministrator, changeSecondryAdministrator, + changeAutoFileDelete, + changeFileRetentionDays, cleanupApps, } = accountSlice.actions; export default accountSlice.reducer; diff --git a/dictation_client/src/features/account/operations.ts b/dictation_client/src/features/account/operations.ts index bbe1f09..c7ccaad 100644 --- a/dictation_client/src/features/account/operations.ts +++ b/dictation_client/src/features/account/operations.ts @@ -9,6 +9,7 @@ import { UpdateAccountInfoRequest, UsersApi, DeleteAccountRequest, + UpdateFileDeleteSettingRequest, } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ViewAccountRelationsInfo } from "./types"; @@ -112,6 +113,58 @@ export const updateAccountInfoAsync = createAsyncThunk< } }); +export const updateFileDeleteSettingAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { autoFileDelete: boolean; fileRetentionDays: number }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/updateFileDeleteSettingAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const requestParam: UpdateFileDeleteSettingRequest = { + autoFileDelete: args.autoFileDelete, + retentionDays: args.fileRetentionDays, + }; + + try { + await accountApi.updateFileDeleteSetting(requestParam, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + const errorMessage = getTranslationID("common.message.internalServerError"); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + export const deleteAccountAsync = createAsyncThunk< { /* Empty Object */ diff --git a/dictation_client/src/features/account/selectors.ts b/dictation_client/src/features/account/selectors.ts index e99fdc1..c48a569 100644 --- a/dictation_client/src/features/account/selectors.ts +++ b/dictation_client/src/features/account/selectors.ts @@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) => state.account.apps.isLoading; export const selectUpdateAccountInfo = (state: RootState) => state.account.apps.updateAccountInfo; +export const selectFileDeleteSetting = (state: RootState) => { + const { autoFileDelete, fileRetentionDays } = state.account.apps; + return { + autoFileDelete, + fileRetentionDays, + }; +}; +export const selectInputValidationErrors = (state: RootState) => { + const { fileRetentionDays } = state.account.apps; + const hasFileRetentionDaysError = + fileRetentionDays <= 0 || fileRetentionDays >= 1000; + return { + hasFileRetentionDaysError, + }; +}; diff --git a/dictation_client/src/features/account/state.ts b/dictation_client/src/features/account/state.ts index 6d3cbe7..0876512 100644 --- a/dictation_client/src/features/account/state.ts +++ b/dictation_client/src/features/account/state.ts @@ -19,4 +19,6 @@ export interface Domain { export interface Apps { updateAccountInfo: UpdateAccountInfoRequest; isLoading: boolean; + autoFileDelete: boolean; + fileRetentionDays: number; } diff --git a/dictation_client/src/features/dictation/constants.ts b/dictation_client/src/features/dictation/constants.ts index f7e22d1..ac273ce 100644 --- a/dictation_client/src/features/dictation/constants.ts +++ b/dictation_client/src/features/dictation/constants.ts @@ -6,7 +6,7 @@ export const STATUS = { BACKUP: "Backup", } as const; -export type StatusType = typeof STATUS[keyof typeof STATUS]; +export type StatusType = (typeof STATUS)[keyof typeof STATUS]; export const LIMIT_TASK_NUM = 100; @@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = { TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE", } as const; export type SortableColumnType = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const isSortableColumnType = ( value: string @@ -36,14 +36,14 @@ export const isSortableColumnType = ( }; export type SortableColumnList = - typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN]; + (typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN]; export const DIRECTION = { ASC: "ASC", DESC: "DESC", } as const; -export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION]; +export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION]; // DirectionTypeの型チェック関数 export const isDirectionType = (arg: string): arg is DirectionType => diff --git a/dictation_client/src/features/dictation/dictationSlice.ts b/dictation_client/src/features/dictation/dictationSlice.ts index c434639..e8361af 100644 --- a/dictation_client/src/features/dictation/dictationSlice.ts +++ b/dictation_client/src/features/dictation/dictationSlice.ts @@ -11,6 +11,8 @@ import { playbackAsync, updateAssigneeAsync, cancelAsync, + deleteTaskAsync, + renameFileAsync, } from "./operations"; import { SORTABLE_COLUMN, @@ -218,6 +220,25 @@ export const dictationSlice = createSlice({ builder.addCase(backupTasksAsync.rejected, (state) => { state.apps.isDownloading = false; }); + builder.addCase(deleteTaskAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTaskAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTaskAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + + builder.addCase(renameFileAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(renameFileAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(renameFileAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/dictation/operations.ts b/dictation_client/src/features/dictation/operations.ts index 88b3001..a25c5d3 100644 --- a/dictation_client/src/features/dictation/operations.ts +++ b/dictation_client/src/features/dictation/operations.ts @@ -565,10 +565,21 @@ export const backupTasksAsync = createAsyncThunk< a.click(); a.parentNode?.removeChild(a); - // eslint-disable-next-line no-await-in-loop - await tasksApi.backup(task.audioFileId, { - headers: { authorization: `Bearer ${accessToken}` }, - }); + // バックアップ済みに更新 + try { + // eslint-disable-next-line no-await-in-loop + await tasksApi.backup(task.audioFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + if (error.code === "E010603") { + // タスクが削除済みの場合は成功扱いとする + } else { + throw e; + } + } } } @@ -580,8 +591,22 @@ export const backupTasksAsync = createAsyncThunk< ); return {}; } catch (e) { - // e ⇒ errorObjectに変換" + // e ⇒ errorObjectに変換 const error = createErrorObject(e); + if (error.code === "E010603") { + // 存在しない音声ファイルをダウンロードしようとした場合 + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID( + "dictationPage.message.fileAlreadyDeletedError" + ), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } + thunkApi.dispatch( openSnackbar({ level: "error", @@ -592,3 +617,143 @@ export const backupTasksAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTaskAsync = createAsyncThunk< + { + // empty + }, + { + // パラメータ + audioFileId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("dictations/deleteTaskAsync", async (args, thunkApi) => { + const { audioFileId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const tasksApi = new TasksApi(config); + + try { + await tasksApi.deleteTask(audioFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let message = getTranslationID("common.message.internalServerError"); + + if (error.statusCode === 400) { + if (error.code === "E010603") { + // タスクが削除済みの場合は成功扱いとする + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + if (error.code === "E010601") { + // タスクがInprogressの場合はエラー + message = getTranslationID("dictationPage.message.deleteFailedError"); + } + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +export const renameFileAsync = createAsyncThunk< + { + // empty + }, + { + // パラメータ + audioFileId: number; + fileName: string; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("dictations/renameFileAsync", async (args, thunkApi) => { + const { audioFileId, fileName } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const filesApi = new FilesApi(config); + + try { + await filesApi.fileRename( + { fileName, audioFileId }, + { headers: { authorization: `Bearer ${accessToken}` } } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let message = getTranslationID("common.message.internalServerError"); + + // 変更権限がない場合はエラー + if (error.code === "E021001") { + message = getTranslationID("dictationPage.message.fileRenameFailedError"); + } + + // ファイル名が既に存在する場合はエラー + if (error.code === "E021002") { + message = getTranslationID( + "dictationPage.message.fileNameAleadyExistsError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts index f4530e7..06781a3 100644 --- a/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts +++ b/dictation_client/src/features/license/licenseCardActivate/licenseCardActivateSlice.ts @@ -1,5 +1,6 @@ import { createSlice } from "@reduxjs/toolkit"; import { LicenseCardActivateState } from "./state"; +import { activateCardLicenseAsync } from "./operations"; const initialState: LicenseCardActivateState = { apps: { @@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({ state.apps = initialState.apps; }, }, + extraReducers: (builder) => { + builder.addCase(activateCardLicenseAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(activateCardLicenseAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(activateCardLicenseAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + }, }); export const { cleanupApps } = licenseCardActivateSlice.actions; diff --git a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts index e9d9816..283661a 100644 --- a/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts +++ b/dictation_client/src/features/license/licenseSummary/licenseSummarySlice.ts @@ -1,6 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; import { LicenseSummaryState } from "./state"; -import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations"; +import { + getCompanyNameAsync, + getLicenseSummaryAsync, + updateRestrictionStatusAsync, +} from "./operations"; const initialState: LicenseSummaryState = { domain: { @@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({ }, }, extraReducers: (builder) => { + builder.addCase(getLicenseSummaryAsync.pending, (state) => { + state.apps.isLoading = true; + }); builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => { state.domain.licenseSummaryInfo = action.payload; + state.apps.isLoading = false; }); + builder.addCase(getLicenseSummaryAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + // 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない + // 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。 builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => { state.domain.accountInfo.companyName = action.payload.companyName; }); + builder.addCase(updateRestrictionStatusAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(updateRestrictionStatusAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/license/licenseSummary/operations.ts b/dictation_client/src/features/license/licenseSummary/operations.ts index a5f18f8..f36966f 100644 --- a/dictation_client/src/features/license/licenseSummary/operations.ts +++ b/dictation_client/src/features/license/licenseSummary/operations.ts @@ -8,6 +8,7 @@ import { GetCompanyNameResponse, GetLicenseSummaryResponse, PartnerLicenseInfo, + UpdateRestrictionStatusRequest, } from "../../../api/api"; import { Configuration } from "../../../api/configuration"; import { ErrorObject, createErrorObject } from "../../../common/errors"; @@ -123,3 +124,58 @@ export const getCompanyNameAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const updateRestrictionStatusAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + accountId: number; + restricted: boolean; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const requestParam: UpdateRestrictionStatusRequest = { + accountId: args.accountId, + restricted: args.restricted, + }; + + try { + await accountApi.updateRestrictionStatus(requestParam, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + // このAPIでは個別のエラーメッセージは不要 + const errorMessage = getTranslationID("common.message.internalServerError"); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/licenseSummary/selectors.ts b/dictation_client/src/features/license/licenseSummary/selectors.ts index 79ba5e9..d760a9e 100644 --- a/dictation_client/src/features/license/licenseSummary/selectors.ts +++ b/dictation_client/src/features/license/licenseSummary/selectors.ts @@ -1,10 +1,11 @@ import { RootState } from "app/store"; // 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する -export const selecLicenseSummaryInfo = (state: RootState) => +export const selectLicenseSummaryInfo = (state: RootState) => state.licenseSummary.domain.licenseSummaryInfo; export const selectCompanyName = (state: RootState) => state.licenseSummary.domain.accountInfo.companyName; -export const selectIsLoading = (state: RootState) => state.license; +export const selectIsLoading = (state: RootState) => + state.licenseSummary.apps.isLoading; diff --git a/dictation_client/src/features/license/partnerLicense/operations.ts b/dictation_client/src/features/license/partnerLicense/operations.ts index fbff656..84b5e73 100644 --- a/dictation_client/src/features/license/partnerLicense/operations.ts +++ b/dictation_client/src/features/license/partnerLicense/operations.ts @@ -105,3 +105,82 @@ export const getPartnerLicenseAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const switchParentAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + // パラメータ + to: number; + children: number[]; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("accounts/switchParentAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + + const { to, children } = args; + + try { + await accountsApi.switchParent( + { + to, + children, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + // TODO:エラー処理 + if (error.code === "E017001") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.accountNotFoundError" + ); + } + + if (error.code === "E017002") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.hierarchyMismatchError" + ); + } + + if (error.code === "E017003") { + errorMessage = getTranslationID( + "changeOwnerPopup.message.regionMismatchError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts index ed843c4..b3c894b 100644 --- a/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts +++ b/dictation_client/src/features/license/partnerLicense/partnerLicenseSlice.ts @@ -1,7 +1,11 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { PartnerLicenseInfo } from "api"; import { PartnerLicensesState, HierarchicalElement } from "./state"; -import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations"; +import { + getMyAccountAsync, + getPartnerLicenseAsync, + switchParentAsync, +} from "./operations"; import { ACCOUNTS_VIEW_LIMIT } from "./constants"; const initialState: PartnerLicensesState = { @@ -12,6 +16,8 @@ const initialState: PartnerLicensesState = { tier: 0, country: "", delegationPermission: false, + autoFileDelete: false, + fileRetentionDays: 0, }, total: 0, ownPartnerLicense: { @@ -107,6 +113,15 @@ export const partnerLicenseSlice = createSlice({ builder.addCase(getPartnerLicenseAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(switchParentAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(switchParentAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(switchParentAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { diff --git a/dictation_client/src/features/partner/operations.ts b/dictation_client/src/features/partner/operations.ts index d59efbe..8c832f6 100644 --- a/dictation_client/src/features/partner/operations.ts +++ b/dictation_client/src/features/partner/operations.ts @@ -8,6 +8,8 @@ import { AccountsApi, CreatePartnerAccountRequest, GetPartnersResponse, + DeletePartnerAccountRequest, + GetPartnerUsersResponse, } from "../../api/api"; import { Configuration } from "../../api/configuration"; @@ -116,3 +118,170 @@ export const getPartnerInfoAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +// パートナーアカウント削除 +export const deletePartnerAccountAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + // パラメータ + accountId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/deletePartnerAccountAsync", async (args, thunkApi) => { + const { accountId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + const deletePartnerAccountRequest: DeletePartnerAccountRequest = { + targetAccountId: accountId, + }; + await accountApi.deletePartnerAccount(deletePartnerAccountRequest, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + if (error.code === "E018001") { + errorMessage = getTranslationID( + "partnerPage.message.partnerDeleteFailedError" + ); + } + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +// パートナーアカウントユーザー取得 +export const getPartnerUsersAsync = createAsyncThunk< + GetPartnerUsersResponse, + { + // パラメータ + accountId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/getPartnerUsersAsync", async (args, thunkApi) => { + const { accountId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + try { + const res = await accountApi.getPartnerUsers( + { targetAccountId: accountId }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + return res.data; + } catch (e) { + const error = createErrorObject(e); + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +// パートナーアカウントユーザー編集 +export const editPartnerInfoAsync = createAsyncThunk< + { + /* Empty Object */ + }, + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("partner/editPartnerInfoAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountApi = new AccountsApi(config); + + const { id, companyName, selectedAdminId } = state.partner.apps.editPartner; + + try { + await accountApi.updatePartnerInfo( + { + targetAccountId: id, + primaryAdminUserId: selectedAdminId, + companyName, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.code === "E010502" || error.code === "E020001") { + errorMessage = getTranslationID("partnerPage.message.editFailedError"); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/partner/partnerSlice.ts b/dictation_client/src/features/partner/partnerSlice.ts index 0eb0ce4..5edf14b 100644 --- a/dictation_client/src/features/partner/partnerSlice.ts +++ b/dictation_client/src/features/partner/partnerSlice.ts @@ -1,6 +1,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { PartnerState } from "./state"; -import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations"; +import { + createPartnerAccountAsync, + getPartnerInfoAsync, + deletePartnerAccountAsync, + getPartnerUsersAsync, + editPartnerInfoAsync, +} from "./operations"; import { LIMIT_PARTNER_VIEW_NUM } from "./constants"; const initialState: PartnerState = { @@ -17,6 +23,13 @@ const initialState: PartnerState = { adminName: "", email: "", }, + editPartner: { + users: [], + id: 0, + companyName: "", + country: "", + selectedAdminId: 0, + }, limit: LIMIT_PARTNER_VIEW_NUM, offset: 0, isLoading: false, @@ -75,6 +88,37 @@ export const partnerSlice = createSlice({ state.apps.delegatedAccountId = undefined; state.apps.delegatedCompanyName = undefined; }, + changeEditPartner: ( + state, + action: PayloadAction<{ + id: number; + companyName: string; + country: string; + }> + ) => { + const { id, companyName, country } = action.payload; + + state.apps.editPartner.id = id; + state.apps.editPartner.companyName = companyName; + state.apps.editPartner.country = country; + }, + changeEditCompanyName: ( + state, + action: PayloadAction<{ companyName: string }> + ) => { + const { companyName } = action.payload; + state.apps.editPartner.companyName = companyName; + }, + changeSelectedAdminId: ( + state, + action: PayloadAction<{ adminId: number }> + ) => { + const { adminId } = action.payload; + state.apps.editPartner.selectedAdminId = adminId; + }, + cleanupPartnerAccount: (state) => { + state.apps.editPartner = initialState.apps.editPartner; + }, }, extraReducers: (builder) => { builder.addCase(createPartnerAccountAsync.pending, (state) => { @@ -97,6 +141,37 @@ export const partnerSlice = createSlice({ builder.addCase(getPartnerInfoAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deletePartnerAccountAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deletePartnerAccountAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deletePartnerAccountAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(getPartnerUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => { + const { users } = action.payload; + state.apps.editPartner.users = users; + state.apps.editPartner.selectedAdminId = + users.find((user) => user.isPrimaryAdmin)?.id ?? 0; + state.apps.isLoading = false; + }); + builder.addCase(getPartnerUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(editPartnerInfoAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(editPartnerInfoAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); export const { @@ -108,5 +183,9 @@ export const { savePageInfo, changeDelegateAccount, cleanupDelegateAccount, + changeEditPartner, + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, } = partnerSlice.actions; export default partnerSlice.reducer; diff --git a/dictation_client/src/features/partner/selectors.ts b/dictation_client/src/features/partner/selectors.ts index 061f8b1..0bd2ac4 100644 --- a/dictation_client/src/features/partner/selectors.ts +++ b/dictation_client/src/features/partner/selectors.ts @@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) => state.partner.apps.delegatedAccountId; export const selectDelegatedCompanyName = (state: RootState) => state.partner.apps.delegatedCompanyName; + +// edit +export const selectEditPartnerId = (state: RootState) => + state.partner.apps.editPartner.id; +export const selectEditPartnerCompanyName = (state: RootState) => + state.partner.apps.editPartner.companyName; +export const selectEditPartnerCountry = (state: RootState) => + state.partner.apps.editPartner.country; + +export const selectEditPartnerUsers = (state: RootState) => + state.partner.apps.editPartner.users; + +export const selectSelectedAdminId = (state: RootState) => + state.partner.apps.editPartner.selectedAdminId; diff --git a/dictation_client/src/features/partner/state.ts b/dictation_client/src/features/partner/state.ts index 18a88dd..0ef2c2c 100644 --- a/dictation_client/src/features/partner/state.ts +++ b/dictation_client/src/features/partner/state.ts @@ -1,6 +1,7 @@ import { CreatePartnerAccountRequest, GetPartnersResponse, + PartnerUser, } from "../../api/api"; export interface PartnerState { @@ -19,4 +20,11 @@ export interface Apps { isLoading: boolean; delegatedAccountId?: number; delegatedCompanyName?: string; + editPartner: { + users: PartnerUser[]; + id: number; + companyName: string; + country: string; + selectedAdminId: number; + }; } diff --git a/dictation_client/src/features/user/operations.ts b/dictation_client/src/features/user/operations.ts index b7ca2b0..6e2cf2c 100644 --- a/dictation_client/src/features/user/operations.ts +++ b/dictation_client/src/features/user/operations.ts @@ -9,6 +9,7 @@ import { UsersApi, LicensesApi, GetAllocatableLicensesResponse, + MultipleImportUser, } from "../../api/api"; import { Configuration } from "../../api/configuration"; import { ErrorObject, createErrorObject } from "../../common/errors"; @@ -383,3 +384,189 @@ export const deallocateLicenseAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteUserAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + { + userId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/deleteUserAsync", async (args, thunkApi) => { + const { userId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + await usersApi.deleteUser( + { + userId, + }, + { + headers: { authorization: `Bearer ${accessToken}` }, + } + ); + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + let errorMessage = getTranslationID("common.message.internalServerError"); + + if (error.statusCode === 400) { + if (error.code === "E014001") { + // ユーザーが削除済みのため成功 + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + } + + // ユーザーに有効なライセンスが割り当たっているため削除不可 + if (error.code === "E014007") { + errorMessage = getTranslationID( + "userListPage.message.userDeletionLicenseActiveError" + ); + } + // 管理者ユーザーため削除不可 + if (error.code === "E014002") { + errorMessage = getTranslationID( + "userListPage.message.adminUserDeletionError" + ); + } + // タイピストユーザーで担当タスクがあるため削除不可 + if (error.code === "E014009") { + errorMessage = getTranslationID( + "userListPage.message.typistUserDeletionTranscriptionTaskError" + ); + } + // タイピストユーザーでルーティングルールに設定されているため削除不可 + if (error.code === "E014004") { + errorMessage = getTranslationID( + "userListPage.message.typistDeletionRoutingRuleError" + ); + } + // タイピストユーザーでTranscriptionistGroupに所属しているため削除不可 + if (error.code === "E014005") { + errorMessage = getTranslationID( + "userListPage.message.typistUserDeletionTranscriptionistGroupError" + ); + } + // Authorユーザーで同一AuthorIDのタスクがあるため削除不可 + if (error.code === "E014006") { + errorMessage = getTranslationID( + "userListPage.message.authorUserDeletionTranscriptionTaskError" + ); + } + // Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可 + if (error.code === "E014003") { + errorMessage = getTranslationID( + "userListPage.message.authorDeletionRoutingRuleError" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: errorMessage, + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); + +export const importUsersAsync = createAsyncThunk< + // 正常時の戻り値の型 + { + /* Empty Object */ + }, + // 引数 + void, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("users/importUsersAsync", async (args, thunkApi) => { + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const { importFileName, importUsers } = state.user.apps; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const usersApi = new UsersApi(config); + + try { + if (importFileName === undefined) { + throw new Error("importFileName is undefined"); + } + + // CSVデータをAPIに送信するためのデータに変換 + const users: MultipleImportUser[] = importUsers.map((user) => ({ + name: user.name ?? "", + email: user.email ?? "", + role: user.role ?? 0, + authorId: user.author_id ?? undefined, + autoRenew: user.auto_assign ?? 0, + notification: user.notification ?? 0, + encryption: user.encryption ?? undefined, + encryptionPassword: user.encryption_password ?? undefined, + prompt: user.prompt ?? undefined, + })); + + await usersApi.multipleImports( + { + filename: importFileName, + users, + }, + { headers: { authorization: `Bearer ${accessToken}` } } + ); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("userListPage.message.importSuccess"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換 + const error = createErrorObject(e); + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message: getTranslationID("common.message.internalServerError"), + }) + ); + + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/user/selectors.ts b/dictation_client/src/features/user/selectors.ts index 29c41fb..8cd9bdf 100644 --- a/dictation_client/src/features/user/selectors.ts +++ b/dictation_client/src/features/user/selectors.ts @@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = ( remaining: undefined, }; }; + +export const selectImportFileName = (state: RootState) => + state.user.apps.importFileName; + +export const selectImportValidationErrors = (state: RootState) => { + const csvUsers = state.user.apps.importUsers; + + let rowNumber = 1; + const invalidInput: number[] = []; + + const duplicatedEmailsMap = new Map(); + const duplicatedAuthorIdsMap = new Map(); + const overMaxRow = csvUsers.length > 100; + + // eslint-disable-next-line no-restricted-syntax + for (const csvUser of csvUsers) { + rowNumber += 1; + + // メールアドレスの重複がある場合、エラーとしてその行番号を追加する + const duplicatedEmailUser = csvUsers.filter( + (x) => x.email === csvUser.email + ); + if (duplicatedEmailUser.length > 1) { + if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) { + duplicatedEmailsMap.set(csvUser.email, rowNumber); + } + } + + // AuthorIDの重複がある場合、エラーとしてその行番号を追加する + const duplicatedAuthorIdUser = csvUsers.filter( + (x) => x.author_id === csvUser.author_id + ); + if (duplicatedAuthorIdUser.length > 1) { + if ( + csvUser.author_id !== null && + !duplicatedAuthorIdsMap.has(csvUser.author_id) + ) { + duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber); + } + } + + // name + if (csvUser.name === null || csvUser.name.length > 225) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // email + const emailPattern = + /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; + if ( + csvUser.name === null || + csvUser.name.length > 225 || + !emailPattern.test(csvUser.email ?? "") + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role + if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // role=1(Author) + if (csvUser.role === 1) { + // author_id + if (csvUser.author_id === null || csvUser.author_id.length > 16) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + // 半角英数字と_の組み合わせで16文字まで + const charaTypePattern = /^[A-Z0-9_]{1,16}$/; + const charaType = new RegExp(charaTypePattern).test(csvUser.author_id); + if (!charaType) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // encryption + if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + if (csvUser.encryption === 1) { + // encryption_password + if (csvUser.encryption === 1) { + const regex = /^[!-~]{4,16}$/; + if (!regex.test(csvUser.encryption_password ?? "")) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + } + + // prompt + if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + // auto_assign + if (csvUser.auto_assign === null || ![0, 1].includes(csvUser.auto_assign)) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + + // notification + if ( + csvUser.notification === null || + ![0, 1].includes(csvUser.notification) + ) { + invalidInput.push(rowNumber); + // eslint-disable-next-line no-continue + continue; + } + } + + const duplicatedEmails = Array.from(duplicatedEmailsMap.values()); + const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values()); + + return { + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + }; +}; diff --git a/dictation_client/src/features/user/state.ts b/dictation_client/src/features/user/state.ts index f25dd94..87e197d 100644 --- a/dictation_client/src/features/user/state.ts +++ b/dictation_client/src/features/user/state.ts @@ -1,3 +1,4 @@ +import { CSVType } from "common/parser"; import { User, AllocatableLicenseInfo } from "../../api/api"; import { AddUser, UpdateUser, LicenseAllocateUser } from "./types"; @@ -19,4 +20,6 @@ export interface Apps { selectedlicenseId: number; hasPasswordMask: boolean; isLoading: boolean; + importFileName: string | undefined; + importUsers: CSVType[]; } diff --git a/dictation_client/src/features/user/types.ts b/dictation_client/src/features/user/types.ts index a96e236..2fa06e7 100644 --- a/dictation_client/src/features/user/types.ts +++ b/dictation_client/src/features/user/types.ts @@ -54,14 +54,14 @@ export interface LicenseAllocateUser { remaining?: number; } -export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES]; +export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES]; // 受け取った値がUSER_ROLESの型であるかどうかを判定する export const isRoleType = (role: string): role is RoleType => Object.values(USER_ROLES).includes(role as RoleType); export type LicenseStatusType = - typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS]; + (typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS]; // 受け取った値がLicenseStatusTypeの型であるかどうかを判定する export const isLicenseStatusType = ( diff --git a/dictation_client/src/features/user/userSlice.ts b/dictation_client/src/features/user/userSlice.ts index 881dba9..98ccfe6 100644 --- a/dictation_client/src/features/user/userSlice.ts +++ b/dictation_client/src/features/user/userSlice.ts @@ -1,5 +1,6 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { USER_ROLES } from "components/auth/constants"; +import { CSVType } from "common/parser"; import { UsersState } from "./state"; import { addUserAsync, @@ -7,6 +8,8 @@ import { updateUserAsync, getAllocatableLicensesAsync, deallocateLicenseAsync, + deleteUserAsync, + importUsersAsync, } from "./operations"; import { RoleType, UserView } from "./types"; @@ -60,6 +63,8 @@ const initialState: UsersState = { selectedlicenseId: 0, hasPasswordMask: false, isLoading: false, + importFileName: undefined, + importUsers: [], }, }; @@ -241,6 +246,21 @@ export const userSlice = createSlice({ state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser; state.apps.selectedlicenseId = initialState.apps.selectedlicenseId; }, + changeImportFileName: ( + state, + action: PayloadAction<{ fileName: string }> + ) => { + const { fileName } = action.payload; + state.apps.importFileName = fileName; + }, + changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => { + const { users } = action.payload; + state.apps.importUsers = users; + }, + cleanupImportUsers: (state) => { + state.apps.importFileName = initialState.apps.importFileName; + state.apps.importUsers = initialState.apps.importUsers; + }, }, extraReducers: (builder) => { builder.addCase(listUsersAsync.pending, (state) => { @@ -290,6 +310,24 @@ export const userSlice = createSlice({ builder.addCase(deallocateLicenseAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deleteUserAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteUserAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteUserAsync.rejected, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(importUsersAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(importUsersAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(importUsersAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); @@ -317,6 +355,9 @@ export const { changeLicenseAllocateUser, changeSelectedlicenseId, cleanupLicenseAllocateInfo, + changeImportFileName, + changeImportCsv, + cleanupImportUsers, } = userSlice.actions; export default userSlice.reducer; diff --git a/dictation_client/src/features/workflow/template/operations.ts b/dictation_client/src/features/workflow/template/operations.ts index 9a15ac2..ae464f3 100644 --- a/dictation_client/src/features/workflow/template/operations.ts +++ b/dictation_client/src/features/workflow/template/operations.ts @@ -115,3 +115,78 @@ export const uploadTemplateAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTemplateAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { templateFileId: number }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/deleteTemplateAsync", async (args, thunkApi) => { + const { templateFileId } = args; + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const templateApi = new TemplatesApi(config); + + try { + // ファイルを削除する + await templateApi.deleteTemplateFile(templateFileId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + if (error.code === "E016001") { + // テンプレートファイルが削除済みの場合は成功扱いとする + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + let message = getTranslationID("common.message.internalServerError"); + + // テンプレートファイルがルーティングルールに紐づく場合はエラー + if (error.code === "E016002") { + message = getTranslationID( + "templateFilePage.message.deleteFailedWorkflowAssigned" + ); + } + // テンプレートファイルが未完了のタスクに紐づく場合はエラー + if (error.code === "E016003") { + message = getTranslationID( + "templateFilePage.message.deleteFailedTaskAssigned" + ); + } + + thunkApi.dispatch( + openSnackbar({ + level: "error", + message, + }) + ); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/template/templateSlice.ts b/dictation_client/src/features/workflow/template/templateSlice.ts index bbd4001..d567fe7 100644 --- a/dictation_client/src/features/workflow/template/templateSlice.ts +++ b/dictation_client/src/features/workflow/template/templateSlice.ts @@ -1,6 +1,10 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; import { TemplateState } from "./state"; -import { listTemplateAsync, uploadTemplateAsync } from "./operations"; +import { + deleteTemplateAsync, + listTemplateAsync, + uploadTemplateAsync, +} from "./operations"; const initialState: TemplateState = { apps: { @@ -45,6 +49,15 @@ export const templateSlice = createSlice({ builder.addCase(uploadTemplateAsync.rejected, (state) => { state.apps.isUploading = false; }); + builder.addCase(deleteTemplateAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTemplateAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTemplateAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/workflow/typistGroup/operations.ts b/dictation_client/src/features/workflow/typistGroup/operations.ts index 70594c6..3eebdc6 100644 --- a/dictation_client/src/features/workflow/typistGroup/operations.ts +++ b/dictation_client/src/features/workflow/typistGroup/operations.ts @@ -269,3 +269,70 @@ export const updateTypistGroupAsync = createAsyncThunk< return thunkApi.rejectWithValue({ error }); } }); + +export const deleteTypistGroupAsync = createAsyncThunk< + { + /* Empty Object */ + }, + { + typistGroupId: number; + }, + { + // rejectした時の返却値の型 + rejectValue: { + error: ErrorObject; + }; + } +>("workflow/deleteTypistGroupAsync", async (args, thunkApi) => { + const { typistGroupId } = args; + + // apiのConfigurationを取得する + const { getState } = thunkApi; + const state = getState() as RootState; + const { configuration } = state.auth; + const accessToken = getAccessToken(state.auth); + const config = new Configuration(configuration); + const accountsApi = new AccountsApi(config); + + try { + await accountsApi.deleteTypistGroup(typistGroupId, { + headers: { authorization: `Bearer ${accessToken}` }, + }); + + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } catch (e) { + // e ⇒ errorObjectに変換" + const error = createErrorObject(e); + + // すでに削除されていた場合は成功扱いする + if (error.code === "E015001") { + thunkApi.dispatch( + openSnackbar({ + level: "info", + message: getTranslationID("common.message.success"), + }) + ); + return {}; + } + + // 以下は実際の削除失敗 + let message = getTranslationID("common.message.internalServerError"); + if (error.code === "E015002") + message = getTranslationID( + "typistGroupSetting.message.deleteFailedWorkflowAssigned" + ); + if (error.code === "E015003") + message = getTranslationID( + "typistGroupSetting.message.deleteFailedCheckoutPermissionExisted" + ); + + thunkApi.dispatch(openSnackbar({ level: "error", message })); + return thunkApi.rejectWithValue({ error }); + } +}); diff --git a/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts b/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts index 1574a3f..623cfcf 100644 --- a/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts +++ b/dictation_client/src/features/workflow/typistGroup/typistGroupSlice.ts @@ -6,6 +6,7 @@ import { listTypistGroupsAsync, listTypistsAsync, updateTypistGroupAsync, + deleteTypistGroupAsync, } from "./operations"; const initialState: TypistGroupState = { @@ -106,6 +107,15 @@ export const typistGroupSlice = createSlice({ builder.addCase(updateTypistGroupAsync.rejected, (state) => { state.apps.isLoading = false; }); + builder.addCase(deleteTypistGroupAsync.pending, (state) => { + state.apps.isLoading = true; + }); + builder.addCase(deleteTypistGroupAsync.fulfilled, (state) => { + state.apps.isLoading = false; + }); + builder.addCase(deleteTypistGroupAsync.rejected, (state) => { + state.apps.isLoading = false; + }); }, }); diff --git a/dictation_client/src/features/workflow/worktype/types.ts b/dictation_client/src/features/workflow/worktype/types.ts index 239cb26..c1c8348 100644 --- a/dictation_client/src/features/workflow/worktype/types.ts +++ b/dictation_client/src/features/workflow/worktype/types.ts @@ -2,7 +2,7 @@ import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants"; // OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する export type OptionItemsDefaultValueType = - typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; + (typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE)[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE]; // 受け取った値がOptionItemDefaultValueType型かどうかを判定する export const isOptionItemDefaultValueType = ( diff --git a/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx b/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx new file mode 100644 index 0000000..1153daf --- /dev/null +++ b/dictation_client/src/pages/AccountPage/fileDeleteSettingPopup.tsx @@ -0,0 +1,169 @@ +import React, { useCallback, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectInputValidationErrors, + selectFileDeleteSetting, + updateFileDeleteSettingAsync, + selectIsLoading, + getAccountRelationsAsync, +} from "features/account"; +import { AppDispatch } from "app/store"; +import { useTranslation } from "react-i18next"; +import styles from "../../styles/app.module.scss"; +import { getTranslationID } from "../../translation"; +import close from "../../assets/images/close.svg"; +import { + changeAutoFileDelete, + changeFileRetentionDays, +} from "../../features/account/accountSlice"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface FileDeleteSettingPopupProps { + // eslint-disable-next-line react/no-unused-prop-types + onClose: () => void; +} + +export const FileDeleteSettingPopup: React.FC = ( + props +) => { + const { onClose } = props; + + const dispatch: AppDispatch = useDispatch(); + const [t] = useTranslation(); + + const isLoading = useSelector(selectIsLoading); + const fileDeleteSetting = useSelector(selectFileDeleteSetting); + const { hasFileRetentionDaysError } = useSelector( + selectInputValidationErrors + ); + + const closePopup = useCallback(() => { + if (isLoading) return; + onClose(); + }, [isLoading, onClose]); + + const [isPushSubmitButton, setIsPushSubmitButton] = useState(false); + + const onUpdateFileDeleteSetting = useCallback(async () => { + if (isLoading) return; + setIsPushSubmitButton(true); + if (hasFileRetentionDaysError) { + return; + } + + const { meta } = await dispatch( + updateFileDeleteSettingAsync({ + autoFileDelete: fileDeleteSetting.autoFileDelete, + fileRetentionDays: fileDeleteSetting.fileRetentionDays, + }) + ); + setIsPushSubmitButton(false); + if (meta.requestStatus === "fulfilled") { + closePopup(); + dispatch(getAccountRelationsAsync()); + } + }, [ + closePopup, + dispatch, + fileDeleteSetting.autoFileDelete, + fileDeleteSetting.fileRetentionDays, + hasFileRetentionDaysError, + isLoading, + ]); + + return ( +
+
+

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

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

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

+

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

+ { + dispatch( + changeFileRetentionDays({ + fileRetentionDays: Number(e.target.value), + }) + ); + }} + />{" "} + {t(getTranslationID("fileDeleteSettingPopup.label.days"))} + {isPushSubmitButton && hasFileRetentionDaysError && ( + + {t( + getTranslationID( + "fileDeleteSettingPopup.label.daysValidationError" + ) + )} + + )} +
+
+ +
+ Loading +
+
+
+
+ ); +}; diff --git a/dictation_client/src/pages/AccountPage/index.tsx b/dictation_client/src/pages/AccountPage/index.tsx index 5bfb8b4..e8a118f 100644 --- a/dictation_client/src/pages/AccountPage/index.tsx +++ b/dictation_client/src/pages/AccountPage/index.tsx @@ -23,6 +23,7 @@ import { getTranslationID } from "translation"; import { TIERS } from "components/auth/constants"; import { isApproveTier } from "features/auth"; import { DeleteAccountPopup } from "./deleteAccountPopup"; +import { FileDeleteSettingPopup } from "./fileDeleteSettingPopup"; import progress_activit from "../../assets/images/progress_activit.svg"; const AccountPage: React.FC = (): JSX.Element => { @@ -40,10 +41,17 @@ const AccountPage: React.FC = (): JSX.Element => { const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] = useState(false); + const [isFileDeleteSettingPopupOpen, setIsFileDeleteSettingPopupOpen] = + useState(false); + const onDeleteAccountOpen = useCallback(() => { setIsDeleteAccountPopupOpen(true); }, [setIsDeleteAccountPopupOpen]); + const onDeleteFileDeleteSettingOpen = useCallback(() => { + setIsFileDeleteSettingPopupOpen(true); + }, [setIsFileDeleteSettingPopupOpen]); + // 階層表示用 const tierNames: { [key: number]: string } = { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -89,6 +97,13 @@ const AccountPage: React.FC = (): JSX.Element => { }} /> )} + {isFileDeleteSettingPopupOpen && ( + { + setIsFileDeleteSettingPopupOpen(false); + }} + /> + )}
@@ -102,12 +117,13 @@ const AccountPage: React.FC = (): JSX.Element => {
- {/* File Delete Setting は現状不要のため非表示
  • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} {
- */}
@@ -216,9 +231,23 @@ const AccountPage: React.FC = (): JSX.Element => { ) )} +
)} {!isTier5 &&
-
} +
+ {t( + getTranslationID("accountPage.label.fileRetentionDays") + )} +
+
+ {viewInfo.account.autoFileDelete + ? viewInfo.account.fileRetentionDays + : "-"} +
diff --git a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx index 979bbf1..e3b2e09 100644 --- a/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx +++ b/dictation_client/src/pages/DictationPage/filePropertyPopup.tsx @@ -1,13 +1,15 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import styles from "styles/app.module.scss"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { selectSelectedFileTask, selectIsLoading, PRIORITY, + renameFileAsync, } from "features/dictation"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; +import { AppDispatch } from "app/store"; import close from "../../assets/images/close.svg"; import lock from "../../assets/images/lock.svg"; @@ -19,14 +21,55 @@ interface FilePropertyPopupProps { export const FilePropertyPopup: React.FC = (props) => { const { onClose, isOpen } = props; const [t] = useTranslation(); + const dispatch: AppDispatch = useDispatch(); const isLoading = useSelector(selectIsLoading); + const [isPushSaveButton, setIsPushSaveButton] = useState(false); + // ポップアップを閉じる処理 const closePopup = useCallback(() => { + setIsPushSaveButton(false); onClose(false); }, [onClose]); const selectedFileTask = useSelector(selectSelectedFileTask); + const [fileName, setFileName] = useState(""); + + useEffect(() => { + if (isOpen) { + setFileName(selectedFileTask?.fileName ?? ""); + } + }, [selectedFileTask, isOpen]); + + // ファイル名の保存処理 + const saveFileName = useCallback(async () => { + setIsPushSaveButton(true); + if (fileName.length === 0) { + return; + } + + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch( + renameFileAsync({ + audioFileId: selectedFileTask?.audioFileId ?? 0, + fileName, + }) + ); + + setIsPushSaveButton(false); + + if (meta.requestStatus === "fulfilled") { + onClose(true); + } + }, [t, dispatch, onClose, fileName, selectedFileTask]); + return (
@@ -45,7 +88,41 @@ export const FilePropertyPopup: React.FC = (props) => { {t(getTranslationID("filePropertyPopup.label.general"))}
{t(getTranslationID("dictationPage.label.fileName"))}
-
{selectedFileTask?.fileName.replace(".zip", "") ?? ""}
+
+ setFileName(e.target.value)} + /> + + {isPushSaveButton && fileName.length === 0 && ( + + {t(getTranslationID("common.message.inputEmptyError"))} + + )} +
+
{t(getTranslationID("dictationPage.label.rawFileName"))}
+
{selectedFileTask?.rawFileName ?? ""}
{t(getTranslationID("dictationPage.label.fileSize"))}
{selectedFileTask?.fileSize ?? ""}
{t(getTranslationID("dictationPage.label.fileLength"))}
diff --git a/dictation_client/src/pages/DictationPage/index.tsx b/dictation_client/src/pages/DictationPage/index.tsx index 07959ff..1c8a47b 100644 --- a/dictation_client/src/pages/DictationPage/index.tsx +++ b/dictation_client/src/pages/DictationPage/index.tsx @@ -33,6 +33,7 @@ import { playbackAsync, cancelAsync, PRIORITY, + deleteTaskAsync, isSortableColumnType, isDirectionType, } from "features/dictation"; @@ -63,6 +64,8 @@ const DictationPage: React.FC = (): JSX.Element => { const isTypist = isTypistUser(); const isNone = !isAuthor && !isTypist; + const isDeletableRole = isAdmin || isAuthor; + // popup制御関係 const [ isChangeTranscriptionistPopupOpen, @@ -496,9 +499,39 @@ const DictationPage: React.FC = (): JSX.Element => { setIsBackupPopupOpen(true); }, []); - const onCloseFilePropertyPopup = useCallback(() => { - setIsFilePropertyPopupOpen(false); - }, []); + const onCloseFilePropertyPopup = useCallback( + (isChanged: boolean) => { + if (isChanged) { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + } + setIsFilePropertyPopupOpen(false); + }, + [ + dispatch, + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup, + sortDirection, + sortableParamName, + ] + ); const sortIconClass = ( currentParam: SortableColumnType, @@ -514,6 +547,53 @@ const DictationPage: React.FC = (): JSX.Element => { return styles.isActiveAz; }; + const onDeleteTask = useCallback( + async (audioFileId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + const { meta } = await dispatch( + deleteTaskAsync({ + audioFileId, + }) + ); + if (meta.requestStatus === "fulfilled") { + const filter = getFilter( + filterUploaded, + filterInProgress, + filterPending, + filterFinished, + filterBackup + ); + dispatch( + listTasksAsync({ + limit: LIMIT_TASK_NUM, + offset: 0, + filter, + direction: sortDirection, + paramName: sortableParamName, + }) + ); + dispatch(listTypistsAsync()); + dispatch(listTypistGroupsAsync()); + } + }, + [ + dispatch, + filterBackup, + filterFinished, + filterInProgress, + filterPending, + filterUploaded, + sortDirection, + sortableParamName, + t, + ] + ); + // 初回読み込み処理 useEffect(() => { (async () => { @@ -1183,17 +1263,26 @@ const DictationPage: React.FC = (): JSX.Element => { )} - {/* タスク削除はCCB後回し分なので今は非表示
  • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + onDeleteTask(x.audioFileId)} + > {t( getTranslationID( "dictationPage.label.deleteDictation" ) )} -
  • - */} + {displayColumn.JobNumber && ( @@ -1252,9 +1341,7 @@ const DictationPage: React.FC = (): JSX.Element => { {x.workType} )} {displayColumn.FileName && ( - - {x.fileName.replace(".zip", "")} - + {x.fileName} )} {displayColumn.FileLength && ( {x.audioDuration} diff --git a/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx b/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx new file mode 100644 index 0000000..04084f1 --- /dev/null +++ b/dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx @@ -0,0 +1,203 @@ +import React, { useState, useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectChildrenPartnerLicenses, + selectIsLoading, + selectOwnPartnerLicense, +} from "features/license/partnerLicense/selectors"; +import { + getMyAccountAsync, + switchParentAsync, +} from "features/license/partnerLicense/operations"; +import { useTranslation } from "react-i18next"; +import { getTranslationID } from "translation"; +import { AppDispatch } from "app/store"; +import { clearHierarchicalElement } from "features/license/partnerLicense"; +import styles from "../../styles/app.module.scss"; +import close from "../../assets/images/close.svg"; +import shuffle from "../../assets/images/shuffle.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface ChangeOwnerPopupProps { + onClose: () => void; +} + +const ChangeOwnerPopup: React.FC = (props) => { + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + + const [selectedChildId, setSelectedChildId] = useState(null); + const [selectedChildName, setSelectedChildName] = useState(""); + const [destinationParentId, setDestinationParentId] = useState(""); + const [error, setError] = useState(""); + + const originParentLicenseInfo = useSelector(selectOwnPartnerLicense); + const childrenLicenseInfos = useSelector(selectChildrenPartnerLicenses); + const isLoading = useSelector(selectIsLoading); + + const { onClose } = props; + const closePopup = useCallback(() => { + if (isLoading) return; + onClose(); + }, [isLoading, onClose]); + + const bulkDisplayName = "-- Bulk --"; + const bulkValue = "bulk"; + + const onBulkChange = useCallback( + (e: React.ChangeEvent) => { + const { value } = e.target; + const childId = value === bulkValue ? null : Number(value); + setSelectedChildId(childId); + + // 一括追加のときは子アカウント名を表示しない + let childName = ""; + if (childId) { + const child = childrenLicenseInfos.find((c) => c.accountId === childId); + // childがundefinedになることはないが、コード解析対応のためのチェック + if (child) { + childName = child.companyName; + } + } + setSelectedChildName(childName); + }, + [childrenLicenseInfos] + ); + + const onSaveClick = useCallback(async () => { + const destinationParentIdNum = Number(destinationParentId); + if ( + Number.isNaN(destinationParentIdNum) || // 数値でない場合 + destinationParentIdNum <= 0 || // IDにならない数値の場合 + destinationParentId.length > 7 // 8桁以上の場合(本システムの特徴として8桁以上になることはあり得ない) + ) { + setError(t(getTranslationID("changeOwnerPopup.label.invalidInputError"))); + return; + } + setError(""); + if ( + // eslint-disable-next-line no-alert + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const children = selectedChildId + ? [selectedChildId] + : childrenLicenseInfos.map((child) => child.accountId); + const { meta } = await dispatch( + switchParentAsync({ to: Number(destinationParentId), children }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(getMyAccountAsync()); + dispatch(clearHierarchicalElement()); + closePopup(); + } + }, [ + childrenLicenseInfos, + closePopup, + destinationParentId, + dispatch, + selectedChildId, + t, + ]); + + return ( +
    +
    +

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

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

    + + + {originParentLicenseInfo.companyName} + +

    +

    +

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

    +
    +
    + +
    +
    + {t(getTranslationID("changeOwnerPopup.label.lowerLayerId"))} +
    +
    + + {selectedChildName} +
    +
    + {/* 処理中や子アカウントが1件も存在しない場合、Saveボタンを押せないようにする */} + 0 + ? styles.isActive + : "" + }`} + onClick={onSaveClick} + disabled={isLoading || childrenLicenseInfos.length <= 0} + /> +
    + + Loading +
    +
    +
    +
    + ); +}; + +export default ChangeOwnerPopup; diff --git a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx index a6dc34b..58c96b6 100644 --- a/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx +++ b/dictation_client/src/pages/LicensePage/licenseOrderPopup.tsx @@ -45,9 +45,10 @@ export const LicenseOrderPopup: React.FC = (props) => { // ポップアップを閉じる処理 const closePopup = useCallback(() => { + if (isLoading) return; setIsPushOrderButton(false); onClose(); - }, [onClose]); + }, [isLoading, onClose]); // 画面からのパラメータ const poNumber = useSelector(selectPoNumber); diff --git a/dictation_client/src/pages/LicensePage/licenseSummary.tsx b/dictation_client/src/pages/LicensePage/licenseSummary.tsx index a343fa5..586dca8 100644 --- a/dictation_client/src/pages/LicensePage/licenseSummary.tsx +++ b/dictation_client/src/pages/LicensePage/licenseSummary.tsx @@ -10,12 +10,16 @@ import { useDispatch, useSelector } from "react-redux"; import { getCompanyNameAsync, getLicenseSummaryAsync, - selecLicenseSummaryInfo, + selectLicenseSummaryInfo, selectCompanyName, + selectIsLoading, + updateRestrictionStatusAsync, } from "features/license/licenseSummary"; import { selectSelectedRow } from "features/license/partnerLicense"; import { selectDelegationAccessToken } from "features/auth/selectors"; import { DelegationBar } from "components/delegate"; +import { TIERS } from "components/auth/constants"; +import { isAdminUser, isApproveTier } from "features/auth/utils"; import postAdd from "../../assets/images/post_add.svg"; import history from "../../assets/images/history.svg"; import key from "../../assets/images/key.svg"; @@ -40,6 +44,8 @@ export const LicenseSummary: React.FC = ( // 代行操作用のトークンを取得する const delegationAccessToken = useSelector(selectDelegationAccessToken); + const isLoading = useSelector(selectIsLoading); + // popup制御関係 const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false); const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] = @@ -62,9 +68,12 @@ export const LicenseSummary: React.FC = ( }, [setIsLicenseOrderHistoryOpen]); // apiからの値取得関係 - const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo); + const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo); const companyName = useSelector(selectCompanyName); + const isTier1 = isApproveTier([TIERS.TIER1]); + const isAdmin = isAdminUser(); + useEffect(() => { dispatch(getLicenseSummaryAsync({ selectedRow })); dispatch(getCompanyNameAsync({ selectedRow })); @@ -78,6 +87,35 @@ export const LicenseSummary: React.FC = ( } }, [onReturn]); + const onStorageAvailableChange = useCallback( + async (e: React.ChangeEvent) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm( + t( + getTranslationID( + "LicenseSummaryPage.message.storageUnavalableSwitchingConfirm" + ) + ) + ) + ) { + return; + } + + const restricted = e.target.checked; + const accountId = selectedRow?.accountId; + // 本関数が実行されるときはselectedRowが存在する前提のため、accountIdが存在しない場合の処理は不要 + if (!accountId) return; + const { meta } = await dispatch( + updateRestrictionStatusAsync({ accountId, restricted }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(getLicenseSummaryAsync({ selectedRow })); + } + }, + [dispatch, selectedRow, t] + ); + return ( <> {/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */} @@ -272,6 +310,27 @@ export const LicenseSummary: React.FC = (
    + {isTier1 && isAdmin && ( +

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

    + )}
    @@ -289,17 +348,31 @@ export const LicenseSummary: React.FC = ( ) )} - {/* Storage Usedの値表示をハイフンに置き換え */} - {/*
    {licenseSummaryInfo.storageSize}GB
    */} -
    -
    +
    + {/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */} + {( + licenseSummaryInfo.storageSize / + 1000 / + 1000 / + 1000 + ).toFixed(3)} + GB +
    {t( getTranslationID("LicenseSummaryPage.label.usedSize") )}
    - {/* Storage Usedの値表示をハイフンに置き換え */} - {/*
    {licenseSummaryInfo.usedSize}GB
    */} -
    -
    +
    + {/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */} + {( + licenseSummaryInfo.usedSize / + 1000 / + 1000 / + 1000 + ).toFixed(3)} + GB +
    {t( getTranslationID( diff --git a/dictation_client/src/pages/LicensePage/partnerLicense.tsx b/dictation_client/src/pages/LicensePage/partnerLicense.tsx index acc9801..03cf8aa 100644 --- a/dictation_client/src/pages/LicensePage/partnerLicense.tsx +++ b/dictation_client/src/pages/LicensePage/partnerLicense.tsx @@ -12,6 +12,7 @@ import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup"; import postAdd from "../../assets/images/post_add.svg"; import history from "../../assets/images/history.svg"; import returnLabel from "../../assets/images/undo.svg"; +import changeOwnerIcon from "../../assets/images/change_circle.svg"; import { isApproveTier } from "../../features/auth/utils"; import { TIERS } from "../../components/auth/constants"; import { @@ -37,6 +38,7 @@ import { LicenseOrderPopup } from "./licenseOrderPopup"; import { LicenseOrderHistory } from "./licenseOrderHistory"; import { LicenseSummary } from "./licenseSummary"; import progress_activit from "../../assets/images/progress_activit.svg"; +import ChangeOwnerPopup from "./changeOwnerPopup"; const PartnerLicense: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -49,6 +51,7 @@ const PartnerLicense: React.FC = (): JSX.Element => { const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] = useState(false); const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false); + const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false); // 階層表示用 const tierNames: { [key: number]: string } = { @@ -148,6 +151,11 @@ const PartnerLicense: React.FC = (): JSX.Element => { [dispatch, setIslicenseOrderHistoryOpen] ); + // changeOwnerボタン押下時 + const onClickChangeOwner = useCallback(() => { + setIsChangeOwnerPopupOpen(true); + }, [setIsChangeOwnerPopupOpen]); + // マウント時のみ実行 useEffect(() => { dispatch(getMyAccountAsync()); @@ -245,6 +253,13 @@ const PartnerLicense: React.FC = (): JSX.Element => { }} /> )} + {isChangeOwnerPopupOpen && ( + { + setIsChangeOwnerPopupOpen(false); + }} + /> + )} {!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
    @@ -329,6 +344,26 @@ const PartnerLicense: React.FC = (): JSX.Element => { )} +
  • + {isVisibleChangeOwner(ownPartnerLicenseInfo.tier) && ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions + + + {t( + getTranslationID( + "partnerLicense.label.changeOwnerButton" + ) + )} + + )} +
    • {hierarchicalElements.map((value) => ( @@ -545,4 +580,10 @@ const PartnerLicense: React.FC = (): JSX.Element => { ); }; +const isVisibleChangeOwner = (partnerTier: number) => + // 自身が第一階層または第二階層で、表示しているパートナーが第三階層または第四階層の場合のみ表示 + isApproveTier([TIERS.TIER1, TIERS.TIER2]) && + (partnerTier.toString() === TIERS.TIER3 || + partnerTier.toString() === TIERS.TIER4); + export default PartnerLicense; diff --git a/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx new file mode 100644 index 0000000..5736ac5 --- /dev/null +++ b/dictation_client/src/pages/PartnerPage/editPartnerAccountPopup.tsx @@ -0,0 +1,178 @@ +import { AppDispatch } from "app/store"; +import React, { useCallback, useEffect } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + changeEditCompanyName, + changeSelectedAdminId, + cleanupPartnerAccount, + getPartnerUsersAsync, + editPartnerInfoAsync, + selectEditPartnerCompanyName, + selectEditPartnerCountry, + selectEditPartnerId, + selectEditPartnerUsers, + selectIsLoading, + selectSelectedAdminId, + selectOffset, + getPartnerInfoAsync, + LIMIT_PARTNER_VIEW_NUM, +} from "features/partner"; +import close from "../../assets/images/close.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; +import { COUNTRY_LIST } from "../SignupPage/constants"; + +interface EditPartnerAccountPopup { + isOpen: boolean; + onClose: () => void; +} + +export const EditPartnerAccountPopup: React.FC = ( + props +) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + const isLoading = useSelector(selectIsLoading); + const offset = useSelector(selectOffset); + + const partnerId = useSelector(selectEditPartnerId); + const companyName = useSelector(selectEditPartnerCompanyName); + const country = useSelector(selectEditPartnerCountry); + + const users = useSelector(selectEditPartnerUsers); + const adminUser = users.find((user) => user.isPrimaryAdmin); + + const selectedAdminId = useSelector(selectSelectedAdminId); + + // ポップアップを閉じる処理 + const closePopup = useCallback(() => { + if (isLoading) { + return; + } + dispatch(cleanupPartnerAccount()); + onClose(); + }, [isLoading, onClose, dispatch]); + + useEffect(() => { + if (isOpen) { + dispatch(getPartnerUsersAsync({ accountId: partnerId })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); + + const onEditPartner = useCallback(async () => { + // eslint-disable-next-line no-alert + if (!window.confirm(t(getTranslationID("common.message.dialogConfirm")))) { + return; + } + + const { meta } = await dispatch(editPartnerInfoAsync()); + if (meta.requestStatus === "fulfilled") { + dispatch( + getPartnerInfoAsync({ + limit: LIMIT_PARTNER_VIEW_NUM, + offset, + }) + ); + closePopup(); + } + }, [dispatch, closePopup, t, offset]); + + return ( +
      +
      +

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

      +
      +
      +
      + {t(getTranslationID("partnerPage.label.accountInformation"))} +
      +
      {t(getTranslationID("partnerPage.label.name"))}
      +
      + { + dispatch( + changeEditCompanyName({ companyName: e.target.value }) + ); + }} + /> +
      +
      {t(getTranslationID("partnerPage.label.country"))}
      +
      + c.value === country)?.label} + className={styles.formInput} + readOnly + /> +
      +
      + {t(getTranslationID("partnerPage.label.primaryAdminInfo"))} +
      +
      {t(getTranslationID("partnerPage.label.adminName"))}
      +
      + +
      +
      {t(getTranslationID("partnerPage.label.email"))}
      +
      + +
      +
      + + Loading +
      +
      +
      +
      +
      + ); +}; diff --git a/dictation_client/src/pages/PartnerPage/index.tsx b/dictation_client/src/pages/PartnerPage/index.tsx index 835cfa3..dac23a2 100644 --- a/dictation_client/src/pages/PartnerPage/index.tsx +++ b/dictation_client/src/pages/PartnerPage/index.tsx @@ -16,23 +16,28 @@ import { selectTotalPage, getPartnerInfoAsync, selectPartnersInfo, + deletePartnerAccountAsync, } from "features/partner/index"; import { changeDelegateAccount, + changeEditPartner, savePageInfo, } from "features/partner/partnerSlice"; import { getTranslationID } from "translation"; import { useTranslation } from "react-i18next"; import { getDelegationTokenAsync } from "features/auth/operations"; import { useNavigate } from "react-router-dom"; +import { Partner } from "api"; import personAdd from "../../assets/images/person_add.svg"; import { TIERS } from "../../components/auth/constants"; import { AddPartnerAccountPopup } from "./addPartnerAccountPopup"; +import { EditPartnerAccountPopup } from "./editPartnerAccountPopup"; import checkFill from "../../assets/images/check_fill.svg"; const PartnerPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); const [isPopupOpen, setIsPopupOpen] = useState(false); + const [isEditPopupOpen, setIsEditPopupOpen] = useState(false); const [t] = useTranslation(); const navigate = useNavigate(); const total = useSelector(selectTotal); @@ -71,6 +76,19 @@ const PartnerPage: React.FC = (): JSX.Element => { const onOpen = useCallback(() => { setIsPopupOpen(true); }, [setIsPopupOpen]); + const onOpenEditPopup = useCallback( + (editPartner: Partner) => { + dispatch( + changeEditPartner({ + id: editPartner.accountId, + companyName: editPartner.name, + country: editPartner.country, + }) + ); + setIsEditPopupOpen(true); + }, + [setIsEditPopupOpen, dispatch] + ); // パートナー取得APIを呼び出す useEffect(() => { @@ -109,6 +127,31 @@ const PartnerPage: React.FC = (): JSX.Element => { [dispatch, navigate, t] ); + // delete account押下時処理 + const onDeleteAccount = useCallback( + async (accountId: number, companyName: string) => { + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm( + `${t( + getTranslationID("partnerPage.message.partnerDeleteConfirm") + )} ${companyName}` + ) + ) { + return; + } + + const { meta } = await dispatch(deletePartnerAccountAsync({ accountId })); + if (meta.requestStatus === "fulfilled") { + dispatch( + getPartnerInfoAsync({ limit: LIMIT_PARTNER_VIEW_NUM, offset }) + ); + } + }, + [dispatch, t, offset] + ); + // HTML return ( <> @@ -118,6 +161,12 @@ const PartnerPage: React.FC = (): JSX.Element => { setIsPopupOpen(false); }} /> + { + setIsEditPopupOpen(false); + }} + />
      @@ -185,10 +234,30 @@ const PartnerPage: React.FC = (): JSX.Element => {
        - {/* パートナーアカウント削除はCCB後回し分なので非表示 {isVisibleButton && (
      • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onOpenEditPopup(x); + }} + > + {t( + getTranslationID( + "partnerPage.label.editAccount" + ) + )} + +
      • + )} + {isVisibleButton && ( +
      • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteAccount(x.accountId, x.name); + }} + > {t( getTranslationID( "partnerPage.label.deleteAccount" @@ -197,7 +266,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
      • )} - */} {isVisibleDealerManagement && (
      • {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} diff --git a/dictation_client/src/pages/TemplateFilePage/index.tsx b/dictation_client/src/pages/TemplateFilePage/index.tsx index 59ef540..f50dd0f 100644 --- a/dictation_client/src/pages/TemplateFilePage/index.tsx +++ b/dictation_client/src/pages/TemplateFilePage/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { AppDispatch } from "app/store"; import Header from "components/header"; @@ -13,6 +13,7 @@ import { selectTemplates, listTemplateAsync, selectIsLoading, + deleteTemplateAsync, } from "features/workflow/template"; import { selectDelegationAccessToken } from "features/auth/selectors"; import { DelegationBar } from "components/delegate"; @@ -35,6 +36,23 @@ export const TemplateFilePage: React.FC = () => { dispatch(listTemplateAsync()); }, [dispatch]); + const onDeleteTemplate = useCallback( + async (templateFileId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch(deleteTemplateAsync({ templateFileId })); + if (meta.requestStatus === "fulfilled") { + dispatch(listTemplateAsync()); + } + }, + [dispatch, t] + ); + return ( <> {isShowAddPopup && ( @@ -101,16 +119,17 @@ export const TemplateFilePage: React.FC = () => { {template.name} diff --git a/dictation_client/src/pages/TypistGroupSettingPage/index.tsx b/dictation_client/src/pages/TypistGroupSettingPage/index.tsx index 05cf232..c4f9e1d 100644 --- a/dictation_client/src/pages/TypistGroupSettingPage/index.tsx +++ b/dictation_client/src/pages/TypistGroupSettingPage/index.tsx @@ -11,6 +11,7 @@ import { selectTypistGroups, selectIsLoading, listTypistGroupsAsync, + deleteTypistGroupAsync, } from "features/workflow/typistGroup"; import { AppDispatch } from "app/store"; import { useTranslation } from "react-i18next"; @@ -47,6 +48,25 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => { [setIsEditPopupOpen] ); + const onDeleteTypistGroup = useCallback( + async (typistGroupId: number) => { + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch( + deleteTypistGroupAsync({ typistGroupId }) + ); + if (meta.requestStatus === "fulfilled") { + dispatch(listTypistGroupsAsync()); + } + }, + [dispatch, t] + ); + useEffect(() => { dispatch(listTypistGroupsAsync()); }, [dispatch]); @@ -142,6 +162,17 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => { {t(getTranslationID("common.label.edit"))}
      • +
      • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteTypistGroup(group.id); + }} + > + {t(getTranslationID("common.label.delete"))} + +
      diff --git a/dictation_client/src/pages/UserListPage/importPopup.tsx b/dictation_client/src/pages/UserListPage/importPopup.tsx new file mode 100644 index 0000000..a8be391 --- /dev/null +++ b/dictation_client/src/pages/UserListPage/importPopup.tsx @@ -0,0 +1,258 @@ +import { AppDispatch } from "app/store"; +import React, { useState, useCallback } from "react"; +import styles from "styles/app.module.scss"; +import { useDispatch, useSelector } from "react-redux"; +import { getTranslationID } from "translation"; +import { useTranslation } from "react-i18next"; +import { + selectIsLoading, + importUsersAsync, + changeImportFileName, + changeImportCsv, + selectImportFileName, + selectImportValidationErrors, + cleanupImportUsers, +} from "features/user"; +import { parseCSV } from "common/parser"; +import close from "../../assets/images/close.svg"; +import download from "../../assets/images/download.svg"; +import upload from "../../assets/images/upload.svg"; +import progress_activit from "../../assets/images/progress_activit.svg"; + +interface UserAddPopupProps { + isOpen: boolean; + onClose: () => void; +} + +export const ImportPopup: React.FC = (props) => { + const { isOpen, onClose } = props; + const dispatch: AppDispatch = useDispatch(); + const { t } = useTranslation(); + // AddUserの情報を取得 + + const closePopup = useCallback(() => { + setIsPushImportButton(false); + dispatch(cleanupImportUsers()); + onClose(); + }, [onClose, dispatch]); + + const [isPushImportButton, setIsPushImportButton] = useState(false); + const isLoading = useSelector(selectIsLoading); + + const importFileName = useSelector(selectImportFileName); + const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } = + useSelector(selectImportValidationErrors); + + const onDownloadCsv = useCallback(() => { + // csvファイルダウンロード処理 + const filename = `import_users.csv`; + + const importCsvHeader = [ + "name", + "email", + "role", + "author_id", + "auto_assign", + "notification", + "encryption", + "encryption_password", + "prompt", + ].toString(); + + const blob = new Blob([importCsvHeader], { + type: "mime", + }); + const blobURL = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = blobURL; + a.download = filename; + document.body.appendChild(a); + a.click(); + a.parentNode?.removeChild(a); + }, []); + + // ファイルが選択されたときの処理 + const handleFileChange = useCallback( + async (event: React.ChangeEvent) => { + // 選択されたファイルを取得(複数選択されても先頭を取得) + const file = event.target.files?.[0]; + + // ファイルが選択されていれば、storeに保存 + if (file) { + const text = await file.text(); + const users = await parseCSV(text.trimEnd()); + + dispatch(changeImportCsv({ users })); + dispatch(changeImportFileName({ fileName: file.name })); + } + // 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする + event.target.value = ""; + }, + [dispatch] + ); + + const onImportUsers = useCallback(async () => { + setIsPushImportButton(true); + if ( + invalidInput.length > 0 || + duplicatedEmails.length > 0 || + duplicatedAuthorIds.length > 0 || + overMaxRow + ) { + return; + } + + await dispatch(importUsersAsync()); + setIsPushImportButton(false); + }, [ + dispatch, + invalidInput, + duplicatedEmails, + duplicatedAuthorIds, + overMaxRow, + ]); + + return ( +
      +
      +

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

      +
      +
      +
      + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.downloadCsv"))} + + {t(getTranslationID("userListPage.text.downloadExplain"))} +
      +
      + +
      +
      + {t(getTranslationID("userListPage.label.inputRules"))} +
      +
      {t(getTranslationID("userListPage.label.nameLabel"))}
      +
      {t(getTranslationID("userListPage.text.nameRule"))}
      +
      + {t(getTranslationID("userListPage.label.emailAddressLabel"))} +
      +
      {t(getTranslationID("userListPage.text.emailAddressRule"))}
      +
      {t(getTranslationID("userListPage.label.roleLabel"))}
      +
      {t(getTranslationID("userListPage.text.roleRule"))}
      +
      {t(getTranslationID("userListPage.label.authorIdLabel"))}
      +
      {t(getTranslationID("userListPage.text.authorIdRule"))}
      +
      {t(getTranslationID("userListPage.label.autoRenewLabel"))}
      +
      {t(getTranslationID("userListPage.text.autoRenewRule"))}
      +
      + {t(getTranslationID("userListPage.label.notificationLabel"))} +
      +
      {t(getTranslationID("userListPage.text.notificationRule"))}
      +
      {t(getTranslationID("userListPage.label.encryptionLabel"))}
      +
      {t(getTranslationID("userListPage.text.encryptionRule"))}
      +
      + {t( + getTranslationID("userListPage.label.encryptionPasswordLabel") + )} +
      +
      + {t(getTranslationID("userListPage.text.encryptionPasswordRule"))} +
      +
      {t(getTranslationID("userListPage.label.promptLabel"))}
      +
      {t(getTranslationID("userListPage.text.promptRule"))}
      +
      + {isPushImportButton && overMaxRow && ( + + {t(getTranslationID("userListPage.message.overMaxUserError"))} + + )} + {isPushImportButton && invalidInput.length > 0 && ( + <> + + {t( + getTranslationID("userListPage.message.invalidInputError") + )} + + + {invalidInput.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedEmails.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateEmailError" + ) + )} + + + {duplicatedEmails.map((row) => `L${row}`).join(", ")} + + + )} + {isPushImportButton && duplicatedAuthorIds.length > 0 && ( + <> + + {t( + getTranslationID( + "userListPage.message.duplicateAuthorIdError" + ) + )} + + + {duplicatedAuthorIds.map((row) => `L${row}`).join(", ")} + + + )} +
      +
      + + Loading +
      +
      +
      +
      +
      + ); +}; diff --git a/dictation_client/src/pages/UserListPage/index.tsx b/dictation_client/src/pages/UserListPage/index.tsx index 76b3a8b..c8a2ba4 100644 --- a/dictation_client/src/pages/UserListPage/index.tsx +++ b/dictation_client/src/pages/UserListPage/index.tsx @@ -10,6 +10,7 @@ import { selectUserViews, selectIsLoading, deallocateLicenseAsync, + deleteUserAsync, } from "features/user"; import { useTranslation } from "react-i18next"; import { getTranslationID } from "translation"; @@ -31,9 +32,11 @@ import personAdd from "../../assets/images/person_add.svg"; import checkFill from "../../assets/images/check_fill.svg"; import checkOutline from "../../assets/images/check_outline.svg"; import progress_activit from "../../assets/images/progress_activit.svg"; +import upload from "../../assets/images/upload.svg"; import { UserAddPopup } from "./popup"; import { UserUpdatePopup } from "./updatePopup"; import { AllocateLicensePopup } from "./allocateLicensePopup"; +import { ImportPopup } from "./importPopup"; const UserListPage: React.FC = (): JSX.Element => { const dispatch: AppDispatch = useDispatch(); @@ -45,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => { const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false); const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] = useState(false); + const [isImportPopupOpen, setIsImportPopupOpen] = useState(false); const onOpen = useCallback(() => { setIsPopupOpen(true); @@ -65,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => { }, [setIsAllocateLicensePopupOpen, dispatch] ); + const onImportPopupOpen = useCallback(() => { + setIsImportPopupOpen(true); + }, [setIsImportPopupOpen]); const onLicenseDeallocation = useCallback( async (userId: number) => { @@ -84,6 +91,24 @@ const UserListPage: React.FC = (): JSX.Element => { [dispatch, t] ); + const onDeleteUser = useCallback( + async (userId: number) => { + // ダイアログ確認 + if ( + /* eslint-disable-next-line no-alert */ + !window.confirm(t(getTranslationID("common.message.dialogConfirm"))) + ) { + return; + } + + const { meta } = await dispatch(deleteUserAsync({ userId })); + if (meta.requestStatus === "fulfilled") { + dispatch(listUsersAsync()); + } + }, + [dispatch, t] + ); + useEffect(() => { // ユーザ一覧取得処理を呼び出す dispatch(listUsersAsync()); @@ -115,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => { setIsAllocateLicensePopupOpen(false); }} /> + { + setIsImportPopupOpen(false); + }} + />
      { {t(getTranslationID("userListPage.label.addUser"))} +
    • + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + + + {t(getTranslationID("userListPage.label.bulkImport"))} + +
    @@ -255,9 +296,13 @@ const UserListPage: React.FC = (): JSX.Element => { )} - {/* ユーザー削除 CCB後回し分なので今は非表示
  • - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} + { + onDeleteUser(user.id); + }} + > {t( getTranslationID( "userListPage.label.deleteUser" @@ -265,7 +310,6 @@ const UserListPage: React.FC = (): JSX.Element => { )}
  • - */} diff --git a/dictation_client/src/pages/UserVerifyPage/index.tsx b/dictation_client/src/pages/UserVerifyPage/index.tsx index f573ffb..5e57b3b 100644 --- a/dictation_client/src/pages/UserVerifyPage/index.tsx +++ b/dictation_client/src/pages/UserVerifyPage/index.tsx @@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => { const jwt = query.get("verify") ?? ""; useEffect(() => { - if (!jwt) { - navigate("/mail-confirm/failed"); - } dispatch(userVerifyAsync({ jwt })); - }, [navigate, dispatch, jwt]); + }, [dispatch, jwt]); const verifyState = useSelector(VerifyStateSelector); diff --git a/dictation_client/src/styles/app.module.scss b/dictation_client/src/styles/app.module.scss index 0753e75..36371a3 100644 --- a/dictation_client/src/styles/app.module.scss +++ b/dictation_client/src/styles/app.module.scss @@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop, margin-bottom: 5rem; } +.formList.userImport .formTitle { + padding: 1rem 4% 0; + line-height: 1.2; +} +.formList.userImport dt:not(.formTitle) { + width: 30%; + padding: 0 4% 0 4%; + font-size: 0.9rem; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) { + background: #f0f0f0; +} +.formList.userImport dt:not(.formTitle):nth-of-type(odd) + dd { + background: #f0f0f0; +} +.formList.userImport dd { + width: 58%; + padding: 0.2rem 4% 0.2rem 0; + margin-bottom: 0; + white-space: pre-line; + word-wrap: break-word; + font-size: 0.9rem; + line-height: 1.2; +} +.formList.userImport dd.full { + width: 100%; + padding: 0.2rem 4% 0.2rem 4%; +} +.formList.userImport dd.full .buttonText { + padding: 0 0 0.8rem; +} +.formList.userImport dd .menuLink { + display: inline-block; + margin-bottom: 0.6rem; + padding: 0.5rem 1.5rem 0.5rem 1.3rem; +} + .account .listVertical { margin-bottom: 3rem; } @@ -1857,6 +1894,18 @@ tr.isSelected .menuInTable li a.isDisable { cursor: pointer; } +.license .checkAvail { + height: 30px; + padding: 0 0.3rem 0.3rem 0; + margin-top: -30px; + box-sizing: border-box; +} +.license .checkAvail label { + cursor: pointer; +} +.license .checkAvail label .formCheck { + vertical-align: middle; +} .license .listVertical dd img[src*="circle"] { filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%) hue-rotate(143deg) brightness(96%) contrast(101%); @@ -1950,6 +1999,61 @@ tr.isSelected .menuInTable li a.isDisable { text-align: right; } +.formList dd.ownerChange { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin-bottom: 0; +} +.formList dd.ownerChange p.Owner, +.formList dd.ownerChange p.newOwner { + width: 150px; +} +.formList dd.ownerChange .arrowR { + width: 8%; + height: 20px; + margin-top: 10px; + margin-right: 2%; + background: #e6e6e6; + position: relative; +} +.formList dd.ownerChange .arrowR::after { + content: ""; + border-top: 20px transparent solid; + border-bottom: 20px transparent solid; + border-left: 20px #e6e6e6 solid; + position: absolute; + top: 50%; + right: -15px; + transform: translateY(-50%); +} +.formList dd.ownerChange + .full { + width: 66%; + margin-left: 30%; + margin-bottom: -10px; + text-align: center; +} +.formList dd.ownerChange + .full .transOwner { + width: 100px; +} +.formList dd.lowerTrans { + margin-bottom: 1.5rem; + position: relative; + text-align: center; +} +.formList dd.lowerTrans select, +.formList dd.lowerTrans span { + margin: 0 auto; +} +.formList dd .txName { + display: block; + width: 150px; + padding: 0.2rem 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .dictation .menuAction { margin-top: -1rem; height: 34px; @@ -2272,6 +2376,9 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dt:not(.formTitle):nth-of-type(odd) + dd { background: #f0f0f0; } +.formList.property dt:has(+ dd.hasInput) { + padding-top: 0.4rem; +} .formList.property dd { width: 58%; padding: 0.2rem 4% 0.2rem 0; @@ -2283,6 +2390,16 @@ tr.isSelected .menuInTable li a.isDisable { .formList.property dd img { height: 1.1rem; } +.formList.property dd .formInput.short { + width: 250px; + padding: 0.3rem 0.3rem 0.1rem; +} +.formList.property dd .formSubmit { + min-width: auto; + padding: 0.2rem 0.5rem; + position: absolute; + right: 0.5rem; +} .formList.property dd.full { width: 100%; padding: 0.2rem 4% 0.2rem 4%; diff --git a/dictation_client/src/styles/app.module.scss.d.ts b/dictation_client/src/styles/app.module.scss.d.ts index d6ebd8f..7f380fa 100644 --- a/dictation_client/src/styles/app.module.scss.d.ts +++ b/dictation_client/src/styles/app.module.scss.d.ts @@ -108,11 +108,12 @@ declare const classNames: { readonly clm0: "clm0"; readonly menuInTable: "menuInTable"; readonly isSelected: "isSelected"; + readonly userImport: "userImport"; + readonly menuLink: "menuLink"; readonly odd: "odd"; readonly alignRight: "alignRight"; readonly menuAction: "menuAction"; readonly inTable: "inTable"; - readonly menuLink: "menuLink"; readonly menuIcon: "menuIcon"; readonly colorLink: "colorLink"; readonly isDisable: "isDisable"; @@ -123,10 +124,18 @@ declare const classNames: { readonly txNormal: "txNormal"; readonly manageIcon: "manageIcon"; readonly manageIconClose: "manageIconClose"; + readonly checkAvail: "checkAvail"; readonly history: "history"; readonly cardHistory: "cardHistory"; readonly partner: "partner"; readonly isOpen: "isOpen"; + readonly ownerChange: "ownerChange"; + readonly Owner: "Owner"; + readonly newOwner: "newOwner"; + readonly arrowR: "arrowR"; + readonly transOwner: "transOwner"; + readonly lowerTrans: "lowerTrans"; + readonly txName: "txName"; readonly alignLeft: "alignLeft"; readonly displayOptions: "displayOptions"; readonly tableFilter: "tableFilter"; @@ -192,6 +201,7 @@ declare const classNames: { readonly hideO10: "hideO10"; readonly op10: "op10"; readonly property: "property"; + readonly hasInput: "hasInput"; readonly formChange: "formChange"; readonly chooseMember: "chooseMember"; readonly holdMember: "holdMember"; diff --git a/dictation_client/src/translation/fr.json b/dictation_client/src/translation/fr.json index 578372a..302ef18 100644 --- a/dictation_client/src/translation/fr.json +++ b/dictation_client/src/translation/fr.json @@ -654,4 +654,4 @@ "lowerLayerId": "Lower Layer ID" } } -} +} \ No newline at end of file diff --git a/dictation_function/.devcontainer/Dockerfile b/dictation_function/.devcontainer/Dockerfile index c33b122..97e310d 100644 --- a/dictation_function/.devcontainer/Dockerfile +++ b/dictation_function/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ FROM node:18.17.1-buster RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \ echo "Asia/Tokyo" > /etc/timezone - + # Options for setup script ARG INSTALL_ZSH="true" ARG UPGRADE_PACKAGES="false" @@ -24,6 +24,21 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$ && apt-get install default-jre -y \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts +# COPY --from=golang:1.18-buster /usr/local/go/ /usr/local/go/ +ENV GO111MODULE=auto +COPY library-scripts/go-debian.sh /tmp/library-scripts/ +RUN bash /tmp/library-scripts/go-debian.sh "1.18" "/usr/local/go" "${GOPATH}" "${USERNAME}" "false" \ + && apt-get clean -y && rm -rf /tmp/library-scripts +ENV PATH="/usr/local/go/bin:${PATH}" +RUN mkdir -p /tmp/gotools \ + && cd /tmp/gotools \ + && export GOPATH=/tmp/gotools \ + && export GOCACHE=/tmp/gotools/cache \ + # sql-migrate + && go install github.com/rubenv/sql-migrate/sql-migrate@v1.1.2 \ + && mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \ + && rm -rf /tmp/gotools + # Update NPM RUN npm install -g npm diff --git a/dictation_function/.devcontainer/docker-compose.yml b/dictation_function/.devcontainer/docker-compose.yml index ebcd52d..78d2750 100644 --- a/dictation_function/.devcontainer/docker-compose.yml +++ b/dictation_function/.devcontainer/docker-compose.yml @@ -16,6 +16,15 @@ services: - CHOKIDAR_USEPOLLING=true networks: - external + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - external networks: external: name: omds_network diff --git a/dictation_function/.devcontainer/pipeline-docker-compose.yml b/dictation_function/.devcontainer/pipeline-docker-compose.yml new file mode 100644 index 0000000..16a94b6 --- /dev/null +++ b/dictation_function/.devcontainer/pipeline-docker-compose.yml @@ -0,0 +1,31 @@ +version: "3" + +services: + dictation_function: + build: . + working_dir: /app/dictation_function + volumes: + - ../../:/app + - node_modules:/app/dictation_function/node_modules + environment: + - CHOKIDAR_USEPOLLING=true + depends_on: + - test_mysql_db + networks: + - dictation_function_network + test_mysql_db: + image: mysql:8.0-bullseye + environment: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: odms + MYSQL_USER: user + MYSQL_PASSWORD: password + networks: + - dictation_function_network +networks: + dictation_function_network: + name: test_dictation_function_network + +# Data Volume として永続化する +volumes: + node_modules: diff --git a/dictation_function/.env b/dictation_function/.env index f8bd781..9f8024b 100644 --- a/dictation_function/.env +++ b/dictation_function/.env @@ -1,5 +1,6 @@ DB_HOST=omds-mysql DB_PORT=3306 DB_NAME=omds +DB_NAME_CCB=omds_ccb DB_USERNAME=omdsdbuser DB_PASSWORD=omdsdbpass \ No newline at end of file diff --git a/dictation_function/.env.test b/dictation_function/.env.test new file mode 100644 index 0000000..5026a47 --- /dev/null +++ b/dictation_function/.env.test @@ -0,0 +1,35 @@ +STAGE=local +TENANT_NAME=tenantoname +SIGNIN_FLOW_NAME=b2c_1_signin_dev +ADB2C_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +ADB2C_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +ADB2C_ORIGIN=https://xxxxxxx.b2clogin.com/xxxxxxxx.onmicrosoft.com/b2c_1_signin_dev/ +KEY_VAULT_NAME=xxxxxxxxxxxxxxx +SENDGRID_API_KEY=SG.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +MAIL_FROM=noreply@se0223.com +APP_DOMAIN=http://localhost:8081/ +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass +ADB2C_CACHE_TTL=86400 +STORAGE_ACCOUNT_NAME_US=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_AU=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_EU=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_IMPORT=saxxxxxxxxx +STORAGE_ACCOUNT_NAME_ANALYSIS=saxxxxxxxxx +STORAGE_ACCOUNT_KEY_US=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_AU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_EU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_IMPORT=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_KEY_ANALYSIS=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx== +STORAGE_ACCOUNT_ENDPOINT_US=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_AU=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_EU=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_IMPORT=https://saxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_ANALYSIS=https://saxxxxxxxxx.blob.core.windows.net/ +BASE_PATH=http://localhost:8081 +ACCESS_TOKEN_LIFETIME_WEB=7200000 +JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n" +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n" \ No newline at end of file diff --git a/dictation_function/.gitignore b/dictation_function/.gitignore index b351586..27683c5 100644 --- a/dictation_function/.gitignore +++ b/dictation_function/.gitignore @@ -11,6 +11,8 @@ Publish *.Cache project.lock.json +.test/ + /packages /TestResults diff --git a/dictation_function/codegen.sh b/dictation_function/codegen.sh new file mode 100644 index 0000000..19b9e9a --- /dev/null +++ b/dictation_function/codegen.sh @@ -0,0 +1,2 @@ +npx openapi-generator-cli version-manager set 7.1.0 +npx openapi-generator-cli generate -g typescript-axios -i /app/dictation_function/src/api/odms/openapi.json -o /app/dictation_function/src/api/ diff --git a/dictation_function/openapitools.json b/dictation_function/openapitools.json new file mode 100644 index 0000000..15fef60 --- /dev/null +++ b/dictation_function/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.1.0" + } +} diff --git a/dictation_function/package-lock.json b/dictation_function/package-lock.json index 8ebaf00..499c39b 100644 --- a/dictation_function/package-lock.json +++ b/dictation_function/package-lock.json @@ -9,6 +9,7 @@ "dependencies": { "@azure/functions": "^4.0.0", "@azure/identity": "^3.1.3", + "@azure/storage-blob": "^12.17.0", "@microsoft/microsoft-graph-client": "^3.0.5", "@sendgrid/mail": "^7.7.0", "dotenv": "^16.0.3", @@ -17,11 +18,13 @@ "typeorm": "^0.3.10" }, "devDependencies": { + "@openapitools/openapi-generator-cli": "^2.9.0", "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", + "base64url": "^3.0.1", "jest": "^28.0.3", "redis-mock": "^0.56.3", "rimraf": "^5.0.0", @@ -85,6 +88,103 @@ "node": ">=14.0.0" } }, + "node_modules/@azure/core-http": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@azure/core-http/node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.6.0.tgz", + "integrity": "sha512-PyRNcaIOfMgoUC01/24NoG+k8O81VrKxYARnDlo+Q2xji0/0/j2nIt8BwQh294pb1c5QnXTDPbNR4KzoDKXEoQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.0.0.tgz", + "integrity": "sha512-RP/mR/WJchR+g+nQFJGOec+nzeN/VvjlwbinccoqfhTsTHbb8X5+mLDp48kHT0ueyum0BNSwGm0kX0UZuIqTGg==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.5.0.tgz", + "integrity": "sha512-zqWdVIt+2Z+3wqxEOGzR5hXFZ8MGKK52x4vFLw8n58pR6ZfKRx3EXYTxTaYxYHc/PexPUTyimcTWFJbji9Z6Iw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/core-rest-pipeline": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.12.2.tgz", @@ -261,6 +361,36 @@ "node": ">=0.8.0" } }, + "node_modules/@azure/storage-blob": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", + "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/storage-blob/node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -1454,6 +1584,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -1608,6 +1747,73 @@ } } }, + "node_modules/@nestjs/common": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.0.tgz", + "integrity": "sha512-DGv34UHsZBxCM3H5QGE2XE/+oLJzz5+714JQjBhjD9VccFlQs3LRxo/epso4l7nJIiNlZkPyIUC8WzfU/5RTsQ==", + "dev": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/core": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.0.tgz", + "integrity": "sha512-N06P5ncknW/Pm8bj964WvLIZn2gNhHliCBoAO1LeBvNImYkecqKcrmLbY49Fa1rmMfEM3MuBHeDys3edeuYAOA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, "node_modules/@npmcli/fs": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", @@ -1724,6 +1930,147 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.9.0.tgz", + "integrity": "sha512-KQpftKeiMoH5aEI/amOVLFGkGeT3DyA7Atj7v7l8xT3O9xnIUpoDmMg0WBTEh+NHxEwEAITQNDzr+JLjkXVaKw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@nestjs/axios": "3.0.1", + "@nestjs/common": "10.3.0", + "@nestjs/core": "10.3.0", + "@nuxtjs/opencollective": "0.3.2", + "axios": "1.6.5", + "chalk": "4.1.2", + "commander": "8.3.0", + "compare-versions": "4.1.4", + "concurrently": "6.5.1", + "console.table": "0.10.0", + "fs-extra": "10.1.0", + "glob": "7.2.3", + "inquirer": "8.2.6", + "lodash": "4.17.21", + "reflect-metadata": "0.1.13", + "rxjs": "7.8.1", + "tslib": "2.6.2" + }, + "bin": { + "openapi-generator-cli": "main.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/openapi_generator" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/@nestjs/axios": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", + "dev": true, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@openapitools/openapi-generator-cli/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1984,8 +2331,29 @@ "node_modules/@types/node": { "version": "18.18.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.6.tgz", - "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==", - "dev": true + "integrity": "sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/@types/prettier": { "version": "2.7.3", @@ -2017,6 +2385,14 @@ "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", "dev": true }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.29", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", @@ -2662,6 +3038,50 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2977,6 +3397,12 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -3017,6 +3443,18 @@ "node": ">=6" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cli-highlight": { "version": "2.1.11", "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", @@ -3139,6 +3577,27 @@ "node": ">=10" } }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3219,6 +3678,15 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -3281,6 +3749,21 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/compare-versions": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz", + "integrity": "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==", + "dev": true + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -3292,12 +3775,196 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concurrently": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", + "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/concurrently/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/concurrently/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/concurrently/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/concurrently/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "dev": true + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "devOptional": true }, + "node_modules/console.table": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/console.table/-/console.table-0.10.0.tgz", + "integrity": "sha512-dPyZofqggxuvSf7WXvNjuRfnsOk1YazkVP8FdxH4tcH2c37wc79/Yl6Bhr7Lsu00KMgy2ql/qCMuNu8xctZM8g==", + "dev": true, + "dependencies": { + "easy-table": "1.1.0" + }, + "engines": { + "node": "> 0.10" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -3369,6 +4036,18 @@ "node": ">=0.10.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", @@ -3454,6 +4133,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-table": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz", + "integrity": "sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==", + "dev": true, + "optionalDependencies": { + "wcwidth": ">=1.0.1" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3490,7 +4178,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -3614,6 +4301,32 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3635,6 +4348,30 @@ "bser": "2.1.1" } }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3661,9 +4398,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -3719,6 +4456,20 @@ "url": "https://ko-fi.com/tunnckoCore/commissions" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -4166,6 +4917,102 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ioredis": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", @@ -4247,6 +5094,15 @@ "node": ">=6" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -4280,6 +5136,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -4363,6 +5231,15 @@ "node": ">=8" } }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -5093,6 +5970,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -5213,6 +6102,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -5268,6 +6163,22 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -5661,6 +6572,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "node_modules/mysql2": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.3.3.tgz", @@ -5745,7 +6662,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "devOptional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -6094,6 +7010,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6245,6 +7214,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -6320,6 +7295,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -6354,6 +7337,12 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/qs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", @@ -6513,6 +7502,25 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -6541,6 +7549,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -6815,6 +7841,12 @@ "source-map": "^0.6.0" } }, + "node_modules/spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7291,6 +8323,24 @@ "node": ">=0.8" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7321,8 +8371,16 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "devOptional": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } }, "node_modules/ts-jest": { "version": "28.0.1", @@ -7408,6 +8466,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -7588,6 +8654,18 @@ "node": ">=4.2.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dev": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici": { "version": "5.26.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", @@ -7619,6 +8697,15 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -7692,17 +8779,24 @@ "makeerror": "1.0.12" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "devOptional": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "devOptional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/dictation_function/package.json b/dictation_function/package.json index 15da5ee..52780bd 100644 --- a/dictation_function/package.json +++ b/dictation_function/package.json @@ -9,11 +9,14 @@ "clean": "rimraf dist", "prestart": "npm run clean && npm run build", "start": "func start", - "test": "jest" + "test": "tsc --noEmit && sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test && jest -w 1", + "typecheck": "tsc --noEmit", + "codegen": "sh codegen.sh" }, "dependencies": { "@azure/functions": "^4.0.0", "@azure/identity": "^3.1.3", + "@azure/storage-blob": "^12.17.0", "@microsoft/microsoft-graph-client": "^3.0.5", "@sendgrid/mail": "^7.7.0", "dotenv": "^16.0.3", @@ -22,11 +25,13 @@ "typeorm": "^0.3.10" }, "devDependencies": { + "@openapitools/openapi-generator-cli": "^2.9.0", "@types/jest": "^27.5.0", "@types/node": "18.x", "@types/redis": "^2.8.13", "@types/redis-mock": "^0.17.3", "azure-functions-core-tools": "^4.x", + "base64url": "^3.0.1", "jest": "^28.0.3", "redis-mock": "^0.56.3", "rimraf": "^5.0.0", diff --git a/dictation_function/src/adb2c/types/types.ts b/dictation_function/src/adb2c/types/types.ts index a7261ef..1c5d0f8 100644 --- a/dictation_function/src/adb2c/types/types.ts +++ b/dictation_function/src/adb2c/types/types.ts @@ -1,5 +1,5 @@ export type AdB2cResponse = { - '@odata.context': string; + "@odata.context": string; value: AdB2cUser[]; }; export type AdB2cUser = { diff --git a/dictation_function/src/api/.gitignore b/dictation_function/src/api/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/dictation_function/src/api/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/dictation_function/src/api/.npmignore b/dictation_function/src/api/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/dictation_function/src/api/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/dictation_function/src/api/.openapi-generator-ignore b/dictation_function/src/api/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/dictation_function/src/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/dictation_function/src/api/.openapi-generator/FILES b/dictation_function/src/api/.openapi-generator/FILES new file mode 100644 index 0000000..a80cd4f --- /dev/null +++ b/dictation_function/src/api/.openapi-generator/FILES @@ -0,0 +1,8 @@ +.gitignore +.npmignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/dictation_function/src/api/.openapi-generator/VERSION b/dictation_function/src/api/.openapi-generator/VERSION new file mode 100644 index 0000000..3769235 --- /dev/null +++ b/dictation_function/src/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.1.0 \ No newline at end of file diff --git a/dictation_function/src/api/api.ts b/dictation_function/src/api/api.ts new file mode 100644 index 0000000..b68f729 --- /dev/null +++ b/dictation_function/src/api/api.ts @@ -0,0 +1,8686 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base'; + +/** + * + * @export + * @interface AccessTokenResponse + */ +export interface AccessTokenResponse { + /** + * + * @type {string} + * @memberof AccessTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface Account + */ +export interface Account { + /** + * + * @type {number} + * @memberof Account + */ + 'accountId': number; + /** + * + * @type {string} + * @memberof Account + */ + 'companyName': string; + /** + * + * @type {number} + * @memberof Account + */ + 'tier': number; + /** + * + * @type {string} + * @memberof Account + */ + 'country': string; + /** + * + * @type {number} + * @memberof Account + */ + 'parentAccountId'?: number; + /** + * + * @type {boolean} + * @memberof Account + */ + 'delegationPermission': boolean; + /** + * + * @type {boolean} + * @memberof Account + */ + 'autoFileDelete': boolean; + /** + * + * @type {number} + * @memberof Account + */ + 'fileRetentionDays': number; + /** + * + * @type {number} + * @memberof Account + */ + 'primaryAdminUserId'?: number; + /** + * + * @type {number} + * @memberof Account + */ + 'secondryAdminUserId'?: number; + /** + * + * @type {string} + * @memberof Account + */ + 'parentAccountName'?: string; +} +/** + * + * @export + * @interface ActivateCardLicensesRequest + */ +export interface ActivateCardLicensesRequest { + /** + * + * @type {string} + * @memberof ActivateCardLicensesRequest + */ + 'cardLicenseKey': string; +} +/** + * + * @export + * @interface AllocatableLicenseInfo + */ +export interface AllocatableLicenseInfo { + /** + * + * @type {number} + * @memberof AllocatableLicenseInfo + */ + 'licenseId': number; + /** + * + * @type {string} + * @memberof AllocatableLicenseInfo + */ + 'expiryDate'?: string; +} +/** + * + * @export + * @interface AllocateLicenseRequest + */ +export interface AllocateLicenseRequest { + /** + * ユーザーID + * @type {number} + * @memberof AllocateLicenseRequest + */ + 'userId': number; + /** + * 割り当てるライセンスのID + * @type {number} + * @memberof AllocateLicenseRequest + */ + 'newLicenseId': number; +} +/** + * + * @export + * @interface Assignee + */ +export interface Assignee { + /** + * TypistID(TypistIDかTypistGroupIDのどちらかに値が入る) + * @type {number} + * @memberof Assignee + */ + 'typistUserId'?: number; + /** + * TypistGroupID(TypistGroupIDかTypistIDのどちらかに値が入る) + * @type {number} + * @memberof Assignee + */ + 'typistGroupId'?: number; + /** + * Typist名 / TypistGroup名 + * @type {string} + * @memberof Assignee + */ + 'typistName': string; +} +/** + * + * @export + * @interface AudioDownloadLocationResponse + */ +export interface AudioDownloadLocationResponse { + /** + * Blob StorageにアクセスするためのSASトークン入りのアクセスURL + * @type {string} + * @memberof AudioDownloadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface AudioNextResponse + */ +export interface AudioNextResponse { + /** + * ODMS Cloud上の次の音声ファイルID(存在しなければundefind) + * @type {number} + * @memberof AudioNextResponse + */ + 'nextFileId'?: number; +} +/** + * + * @export + * @interface AudioOptionItem + */ +export interface AudioOptionItem { + /** + * + * @type {string} + * @memberof AudioOptionItem + */ + 'optionItemLabel': string; + /** + * + * @type {string} + * @memberof AudioOptionItem + */ + 'optionItemValue': string; +} +/** + * + * @export + * @interface AudioUploadFinishedRequest + */ +export interface AudioUploadFinishedRequest { + /** + * アップロード先Blob Storage(ファイル名含む) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'url': string; + /** + * 自分自身(ログイン認証)したAuthorID + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'authorId': string; + /** + * 音声ファイル名 + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'fileName': string; + /** + * 音声ファイルの録音時間(ミリ秒の整数値) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'duration': string; + /** + * 音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'createdDate': string; + /** + * 音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'finishedDate': string; + /** + * 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'uploadedDate': string; + /** + * 音声ファイルのファイルサイズ(Byte) + * @type {number} + * @memberof AudioUploadFinishedRequest + */ + 'fileSize': number; + /** + * 優先度 \"00\":Normal / \"01\":High + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'priority': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP) + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'audioFormat': string; + /** + * + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'comment': string; + /** + * + * @type {string} + * @memberof AudioUploadFinishedRequest + */ + 'workType': string; + /** + * 音声ファイルに紐づくOption Itemの一覧(10個固定) + * @type {Array} + * @memberof AudioUploadFinishedRequest + */ + 'optionItemList': Array; + /** + * + * @type {boolean} + * @memberof AudioUploadFinishedRequest + */ + 'isEncrypted': boolean; +} +/** + * + * @export + * @interface AudioUploadFinishedResponse + */ +export interface AudioUploadFinishedResponse { + /** + * 8桁固定の数字 + * @type {string} + * @memberof AudioUploadFinishedResponse + */ + 'jobNumber': string; +} +/** + * + * @export + * @interface AudioUploadLocationResponse + */ +export interface AudioUploadLocationResponse { + /** + * Blob StorageにアクセスするためのSASトークン入りのアクセスURL + * @type {string} + * @memberof AudioUploadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface Author + */ +export interface Author { + /** + * Authorユーザーの内部ID + * @type {number} + * @memberof Author + */ + 'id': number; + /** + * AuthorID + * @type {string} + * @memberof Author + */ + 'authorId': string; +} +/** + * + * @export + * @interface CancelIssueRequest + */ +export interface CancelIssueRequest { + /** + * 注文元アカウントID + * @type {number} + * @memberof CancelIssueRequest + */ + 'orderedAccountId': number; + /** + * POナンバー + * @type {string} + * @memberof CancelIssueRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface CancelOrderRequest + */ +export interface CancelOrderRequest { + /** + * + * @type {string} + * @memberof CancelOrderRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface ConfirmRequest + */ +export interface ConfirmRequest { + /** + * + * @type {string} + * @memberof ConfirmRequest + */ + 'token': string; +} +/** + * + * @export + * @interface CreateAccountRequest + */ +export interface CreateAccountRequest { + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'companyName': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof CreateAccountRequest + */ + 'country': string; + /** + * + * @type {number} + * @memberof CreateAccountRequest + */ + 'dealerAccountId'?: number; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminName': string; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminMail': string; + /** + * + * @type {string} + * @memberof CreateAccountRequest + */ + 'adminPassword': string; + /** + * 同意済み利用規約のバージョン(EULA) + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedEulaVersion': string; + /** + * 同意済みプライバシーポリシーのバージョン + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedPrivacyNoticeVersion': string; + /** + * 同意済み利用規約のバージョン(DPA) + * @type {string} + * @memberof CreateAccountRequest + */ + 'acceptedDpaVersion': string; + /** + * reCAPTCHA Token + * @type {string} + * @memberof CreateAccountRequest + */ + 'token': string; +} +/** + * + * @export + * @interface CreateOrdersRequest + */ +export interface CreateOrdersRequest { + /** + * + * @type {string} + * @memberof CreateOrdersRequest + */ + 'poNumber': string; + /** + * + * @type {number} + * @memberof CreateOrdersRequest + */ + 'orderCount': number; +} +/** + * + * @export + * @interface CreatePartnerAccountRequest + */ +export interface CreatePartnerAccountRequest { + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'companyName': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'country': string; + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'adminName': string; + /** + * + * @type {string} + * @memberof CreatePartnerAccountRequest + */ + 'email': string; +} +/** + * + * @export + * @interface CreateTypistGroupRequest + */ +export interface CreateTypistGroupRequest { + /** + * + * @type {string} + * @memberof CreateTypistGroupRequest + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof CreateTypistGroupRequest + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface CreateWorkflowsRequest + */ +export interface CreateWorkflowsRequest { + /** + * Authorの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'authorId': number; + /** + * Worktypeの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'worktypeId'?: number; + /** + * テンプレートの内部ID + * @type {number} + * @memberof CreateWorkflowsRequest + */ + 'templateId'?: number; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof CreateWorkflowsRequest + */ + 'typists': Array; +} +/** + * + * @export + * @interface CreateWorktypesRequest + */ +export interface CreateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof CreateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof CreateWorktypesRequest + */ + 'description'?: string; +} +/** + * + * @export + * @interface Dealer + */ +export interface Dealer { + /** + * アカウントID + * @type {number} + * @memberof Dealer + */ + 'id': number; + /** + * 会社名 + * @type {string} + * @memberof Dealer + */ + 'name': string; + /** + * 国名(ISO 3166-1 alpha-2) + * @type {string} + * @memberof Dealer + */ + 'country': string; +} +/** + * + * @export + * @interface DeallocateLicenseRequest + */ +export interface DeallocateLicenseRequest { + /** + * ユーザーID + * @type {number} + * @memberof DeallocateLicenseRequest + */ + 'userId': number; +} +/** + * + * @export + * @interface DelegationAccessTokenResponse + */ +export interface DelegationAccessTokenResponse { + /** + * 代行操作用のアクセストークン + * @type {string} + * @memberof DelegationAccessTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface DelegationTokenRequest + */ +export interface DelegationTokenRequest { + /** + * 代行操作対象のアカウントID + * @type {number} + * @memberof DelegationTokenRequest + */ + 'delegatedAccountId': number; +} +/** + * + * @export + * @interface DelegationTokenResponse + */ +export interface DelegationTokenResponse { + /** + * 代行操作用のリフレッシュトークン + * @type {string} + * @memberof DelegationTokenResponse + */ + 'refreshToken': string; + /** + * 代行操作用のアクセストークン + * @type {string} + * @memberof DelegationTokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface DeleteAccountRequest + */ +export interface DeleteAccountRequest { + /** + * アカウントID + * @type {number} + * @memberof DeleteAccountRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface ErrorResponse + */ +export interface ErrorResponse { + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'message': string; + /** + * + * @type {string} + * @memberof ErrorResponse + */ + 'code': string; +} +/** + * + * @export + * @interface GetAccountInfoMinimalAccessRequest + */ +export interface GetAccountInfoMinimalAccessRequest { + /** + * idトークン + * @type {string} + * @memberof GetAccountInfoMinimalAccessRequest + */ + 'idToken': string; +} +/** + * + * @export + * @interface GetAccountInfoMinimalAccessResponse + */ +export interface GetAccountInfoMinimalAccessResponse { + /** + * 階層 + * @type {number} + * @memberof GetAccountInfoMinimalAccessResponse + */ + 'tier': number; +} +/** + * + * @export + * @interface GetAllocatableLicensesResponse + */ +export interface GetAllocatableLicensesResponse { + /** + * + * @type {Array} + * @memberof GetAllocatableLicensesResponse + */ + 'allocatableLicenses': Array; +} +/** + * + * @export + * @interface GetAuthorsResponse + */ +export interface GetAuthorsResponse { + /** + * + * @type {Array} + * @memberof GetAuthorsResponse + */ + 'authors': Array; +} +/** + * + * @export + * @interface GetCompanyNameRequest + */ +export interface GetCompanyNameRequest { + /** + * + * @type {number} + * @memberof GetCompanyNameRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetCompanyNameResponse + */ +export interface GetCompanyNameResponse { + /** + * + * @type {string} + * @memberof GetCompanyNameResponse + */ + 'companyName': string; +} +/** + * + * @export + * @interface GetDealersResponse + */ +export interface GetDealersResponse { + /** + * + * @type {Array} + * @memberof GetDealersResponse + */ + 'dealers': Array; +} +/** + * + * @export + * @interface GetLicenseSummaryRequest + */ +export interface GetLicenseSummaryRequest { + /** + * + * @type {number} + * @memberof GetLicenseSummaryRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetLicenseSummaryResponse + */ +export interface GetLicenseSummaryResponse { + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'totalLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'allocatedLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'reusableLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'freeLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'expiringWithin14daysLicense': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'issueRequesting': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'numberOfRequesting': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'shortage': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'storageSize': number; + /** + * + * @type {number} + * @memberof GetLicenseSummaryResponse + */ + 'usedSize': number; + /** + * + * @type {boolean} + * @memberof GetLicenseSummaryResponse + */ + 'isStorageAvailable': boolean; +} +/** + * + * @export + * @interface GetMyAccountResponse + */ +export interface GetMyAccountResponse { + /** + * + * @type {Account} + * @memberof GetMyAccountResponse + */ + 'account': Account; +} +/** + * + * @export + * @interface GetMyUserResponse + */ +export interface GetMyUserResponse { + /** + * ユーザー名 + * @type {string} + * @memberof GetMyUserResponse + */ + 'userName': string; +} +/** + * + * @export + * @interface GetOptionItemsResponse + */ +export interface GetOptionItemsResponse { + /** + * + * @type {Array} + * @memberof GetOptionItemsResponse + */ + 'optionItems': Array; +} +/** + * + * @export + * @interface GetOrderHistoriesRequest + */ +export interface GetOrderHistoriesRequest { + /** + * 取得件数 + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'limit': number; + /** + * 開始位置 + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'offset': number; + /** + * アカウントID + * @type {number} + * @memberof GetOrderHistoriesRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetOrderHistoriesResponse + */ +export interface GetOrderHistoriesResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetOrderHistoriesResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetOrderHistoriesResponse + */ + 'orderHistories': Array; +} +/** + * + * @export + * @interface GetPartnerLicensesRequest + */ +export interface GetPartnerLicensesRequest { + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'limit': number; + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'offset': number; + /** + * + * @type {number} + * @memberof GetPartnerLicensesRequest + */ + 'accountId': number; +} +/** + * + * @export + * @interface GetPartnerLicensesResponse + */ +export interface GetPartnerLicensesResponse { + /** + * + * @type {number} + * @memberof GetPartnerLicensesResponse + */ + 'total': number; + /** + * + * @type {PartnerLicenseInfo} + * @memberof GetPartnerLicensesResponse + */ + 'ownPartnerLicense': PartnerLicenseInfo; + /** + * + * @type {Array} + * @memberof GetPartnerLicensesResponse + */ + 'childrenPartnerLicenses': Array; +} +/** + * + * @export + * @interface GetPartnersResponse + */ +export interface GetPartnersResponse { + /** + * 合計件数 + * @type {number} + * @memberof GetPartnersResponse + */ + 'total': number; + /** + * + * @type {Array} + * @memberof GetPartnersResponse + */ + 'partners': Array; +} +/** + * + * @export + * @interface GetRelationsResponse + */ +export interface GetRelationsResponse { + /** + * ログインしたユーザーのAuthorID(Authorでない場合はundefined) + * @type {string} + * @memberof GetRelationsResponse + */ + 'authorId'?: string; + /** + * 属しているアカウントのAuthorID List(全て) + * @type {Array} + * @memberof GetRelationsResponse + */ + 'authorIdList': Array; + /** + * アカウントに設定されているWorktypeIDのリスト(最大20個) + * @type {Array} + * @memberof GetRelationsResponse + */ + 'workTypeList': Array; + /** + * ユーザーが音声ファイルを暗号化するかどうか + * @type {boolean} + * @memberof GetRelationsResponse + */ + 'isEncrypted': boolean; + /** + * ユーザーが暗号化を掛ける場合のパスワード + * @type {string} + * @memberof GetRelationsResponse + */ + 'encryptionPassword'?: string; + /** + * アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する) + * @type {string} + * @memberof GetRelationsResponse + */ + 'activeWorktype': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP): DS2固定 + * @type {string} + * @memberof GetRelationsResponse + */ + 'audioFormat': string; + /** + * デバイス上で自動的にWorkTypeの選択画面を表示するかどうかのユーザーごとの設定(Authorでない場合はfalse) + * @type {boolean} + * @memberof GetRelationsResponse + */ + 'prompt': boolean; +} +/** + * + * @export + * @interface GetSortCriteriaResponse + */ +export interface GetSortCriteriaResponse { + /** + * ASC/DESC + * @type {string} + * @memberof GetSortCriteriaResponse + */ + 'direction': string; + /** + * JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @type {string} + * @memberof GetSortCriteriaResponse + */ + 'paramName': string; +} +/** + * + * @export + * @interface GetTemplatesResponse + */ +export interface GetTemplatesResponse { + /** + * テンプレートファイルの一覧 + * @type {Array} + * @memberof GetTemplatesResponse + */ + 'templates': Array; +} +/** + * + * @export + * @interface GetTermsInfoResponse + */ +export interface GetTermsInfoResponse { + /** + * + * @type {Array} + * @memberof GetTermsInfoResponse + */ + 'termsInfo': Array; +} +/** + * + * @export + * @interface GetTypistGroupResponse + */ +export interface GetTypistGroupResponse { + /** + * + * @type {string} + * @memberof GetTypistGroupResponse + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof GetTypistGroupResponse + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface GetTypistGroupsResponse + */ +export interface GetTypistGroupsResponse { + /** + * + * @type {Array} + * @memberof GetTypistGroupsResponse + */ + 'typistGroups': Array; +} +/** + * + * @export + * @interface GetTypistsResponse + */ +export interface GetTypistsResponse { + /** + * + * @type {Array} + * @memberof GetTypistsResponse + */ + 'typists': Array; +} +/** + * + * @export + * @interface GetUsersResponse + */ +export interface GetUsersResponse { + /** + * + * @type {Array} + * @memberof GetUsersResponse + */ + 'users': Array; +} +/** + * + * @export + * @interface GetWorkflowsResponse + */ +export interface GetWorkflowsResponse { + /** + * ワークフローの一覧 + * @type {Array} + * @memberof GetWorkflowsResponse + */ + 'workflows': Array; +} +/** + * + * @export + * @interface GetWorktypeOptionItem + */ +export interface GetWorktypeOptionItem { + /** + * + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'itemLabel': string; + /** + * Default / Blank / LastInput + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'defaultValueType': string; + /** + * + * @type {string} + * @memberof GetWorktypeOptionItem + */ + 'initialValue': string; + /** + * + * @type {number} + * @memberof GetWorktypeOptionItem + */ + 'id': number; +} +/** + * + * @export + * @interface GetWorktypesResponse + */ +export interface GetWorktypesResponse { + /** + * + * @type {Array} + * @memberof GetWorktypesResponse + */ + 'worktypes': Array; + /** + * Active WorktypeIDに設定されているWorkTypeの内部ID + * @type {number} + * @memberof GetWorktypesResponse + */ + 'active'?: number; +} +/** + * + * @export + * @interface IssueCardLicensesRequest + */ +export interface IssueCardLicensesRequest { + /** + * + * @type {number} + * @memberof IssueCardLicensesRequest + */ + 'createCount': number; +} +/** + * + * @export + * @interface IssueCardLicensesResponse + */ +export interface IssueCardLicensesResponse { + /** + * + * @type {Array} + * @memberof IssueCardLicensesResponse + */ + 'cardLicenseKeys': Array; +} +/** + * + * @export + * @interface IssueLicenseRequest + */ +export interface IssueLicenseRequest { + /** + * 注文元アカウントID + * @type {number} + * @memberof IssueLicenseRequest + */ + 'orderedAccountId': number; + /** + * POナンバー + * @type {string} + * @memberof IssueLicenseRequest + */ + 'poNumber': string; +} +/** + * + * @export + * @interface LicenseOrder + */ +export interface LicenseOrder { + /** + * 注文日付 + * @type {string} + * @memberof LicenseOrder + */ + 'orderDate': string; + /** + * 発行日付 + * @type {string} + * @memberof LicenseOrder + */ + 'issueDate'?: string; + /** + * 注文数 + * @type {number} + * @memberof LicenseOrder + */ + 'numberOfOrder': number; + /** + * POナンバー + * @type {string} + * @memberof LicenseOrder + */ + 'poNumber': string; + /** + * 注文状態 + * @type {string} + * @memberof LicenseOrder + */ + 'status': string; +} +/** + * + * @export + * @interface MultipleImportErrors + */ +export interface MultipleImportErrors { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportErrors + */ + 'name': string; + /** + * エラー発生行数 + * @type {number} + * @memberof MultipleImportErrors + */ + 'line': number; + /** + * エラーコード + * @type {string} + * @memberof MultipleImportErrors + */ + 'errorCode': string; +} +/** + * + * @export + * @interface MultipleImportUser + */ +export interface MultipleImportUser { + /** + * ユーザー名 + * @type {string} + * @memberof MultipleImportUser + */ + 'name': string; + /** + * メールアドレス + * @type {string} + * @memberof MultipleImportUser + */ + 'email': string; + /** + * 0(none)/1(author)/2(typist) + * @type {number} + * @memberof MultipleImportUser + */ + 'role': number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'authorId'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'autoRenew': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'notification': number; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'encryption'?: number; + /** + * + * @type {string} + * @memberof MultipleImportUser + */ + 'encryptionPassword'?: string; + /** + * 0(false)/1(true) + * @type {number} + * @memberof MultipleImportUser + */ + 'prompt'?: number; +} +/** + * + * @export + * @interface OptionItem + */ +export interface OptionItem { + /** + * Option Itemのラベル + * @type {string} + * @memberof OptionItem + */ + 'label': string; + /** + * 項目タイプ 1:Blank/2:Default/3:前の値 + * @type {number} + * @memberof OptionItem + */ + 'initialValueType': number; + /** + * typeでDefaultを選択した場合のデフォルト値 + * @type {string} + * @memberof OptionItem + */ + 'defaultValue': string; +} +/** + * + * @export + * @interface OptionItemList + */ +export interface OptionItemList { + /** + * + * @type {string} + * @memberof OptionItemList + */ + 'workTypeId': string; + /** + * 1WorkTypeIDにつき、10個まで登録可能 + * @type {Array} + * @memberof OptionItemList + */ + 'optionItemList': Array; +} +/** + * + * @export + * @interface Partner + */ +export interface Partner { + /** + * 会社名 + * @type {string} + * @memberof Partner + */ + 'name': string; + /** + * 階層 + * @type {number} + * @memberof Partner + */ + 'tier': number; + /** + * アカウントID + * @type {number} + * @memberof Partner + */ + 'accountId': number; + /** + * 国 + * @type {string} + * @memberof Partner + */ + 'country': string; + /** + * プライマリ管理者 + * @type {string} + * @memberof Partner + */ + 'primaryAdmin': string; + /** + * プライマリ管理者メールアドレス + * @type {string} + * @memberof Partner + */ + 'email': string; + /** + * 代行操作許可 + * @type {boolean} + * @memberof Partner + */ + 'dealerManagement': boolean; +} +/** + * + * @export + * @interface PartnerLicenseInfo + */ +export interface PartnerLicenseInfo { + /** + * アカウントID + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'accountId': number; + /** + * 階層 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'tier': number; + /** + * アカウント名 + * @type {string} + * @memberof PartnerLicenseInfo + */ + 'companyName': string; + /** + * 保有している有効期限が未設定あるいは有効期限内のライセンス数 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'stockLicense': number; + /** + * 子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数 + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'issuedRequested': number; + /** + * 不足数({Stock license} - {Issue Requested}) + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'shortage': number; + /** + * 未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数) + * @type {number} + * @memberof PartnerLicenseInfo + */ + 'issueRequesting': number; +} +/** + * + * @export + * @interface PostActiveWorktypeRequest + */ +export interface PostActiveWorktypeRequest { + /** + * Active WorkTypeIDにするWorktypeの内部ID + * @type {number} + * @memberof PostActiveWorktypeRequest + */ + 'id'?: number; +} +/** + * + * @export + * @interface PostCheckoutPermissionRequest + */ +export interface PostCheckoutPermissionRequest { + /** + * 文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧 + * @type {Array} + * @memberof PostCheckoutPermissionRequest + */ + 'assignees': Array; +} +/** + * + * @export + * @interface PostDeleteUserRequest + */ +export interface PostDeleteUserRequest { + /** + * 削除対象のユーザーID + * @type {number} + * @memberof PostDeleteUserRequest + */ + 'userId': number; +} +/** + * + * @export + * @interface PostMultipleImportsCompleteRequest + */ +export interface PostMultipleImportsCompleteRequest { + /** + * アカウントID + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'accountId': number; + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsCompleteRequest + */ + 'filename': string; + /** + * 一括登録受付時刻(UNIXTIME/ミリ秒) + * @type {number} + * @memberof PostMultipleImportsCompleteRequest + */ + 'requestTime': number; + /** + * + * @type {Array} + * @memberof PostMultipleImportsCompleteRequest + */ + 'errors': Array; +} +/** + * + * @export + * @interface PostMultipleImportsRequest + */ +export interface PostMultipleImportsRequest { + /** + * CSVファイル名 + * @type {string} + * @memberof PostMultipleImportsRequest + */ + 'filename': string; + /** + * + * @type {Array} + * @memberof PostMultipleImportsRequest + */ + 'users': Array; +} +/** + * + * @export + * @interface PostSortCriteriaRequest + */ +export interface PostSortCriteriaRequest { + /** + * ASC/DESC + * @type {string} + * @memberof PostSortCriteriaRequest + */ + 'direction': string; + /** + * JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @type {string} + * @memberof PostSortCriteriaRequest + */ + 'paramName': string; +} +/** + * + * @export + * @interface PostUpdateUserRequest + */ +export interface PostUpdateUserRequest { + /** + * + * @type {number} + * @memberof PostUpdateUserRequest + */ + 'id': number; + /** + * none/author/typist + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'role': string; + /** + * + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'authorId'?: string; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'encryption'?: boolean; + /** + * + * @type {string} + * @memberof PostUpdateUserRequest + */ + 'encryptionPassword'?: string; + /** + * + * @type {boolean} + * @memberof PostUpdateUserRequest + */ + 'prompt'?: boolean; +} +/** + * + * @export + * @interface PostWorktypeOptionItem + */ +export interface PostWorktypeOptionItem { + /** + * + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'itemLabel': string; + /** + * Default / Blank / LastInput + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'defaultValueType': string; + /** + * + * @type {string} + * @memberof PostWorktypeOptionItem + */ + 'initialValue': string; +} +/** + * + * @export + * @interface RegisterRequest + */ +export interface RegisterRequest { + /** + * wns or apns + * @type {string} + * @memberof RegisterRequest + */ + 'pns': string; + /** + * wnsのチャネルURI or apnsのデバイストークン + * @type {string} + * @memberof RegisterRequest + */ + 'handler': string; +} +/** + * + * @export + * @interface SignupRequest + */ +export interface SignupRequest { + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof SignupRequest + */ + 'role': string; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'authorId'?: string; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'encryption'?: boolean; + /** + * + * @type {string} + * @memberof SignupRequest + */ + 'encryptionPassword'?: string; + /** + * + * @type {boolean} + * @memberof SignupRequest + */ + 'prompt'?: boolean; +} +/** + * + * @export + * @interface Task + */ +export interface Task { + /** + * ODMS Cloud上の音声ファイルID + * @type {number} + * @memberof Task + */ + 'audioFileId': number; + /** + * AuthorID + * @type {string} + * @memberof Task + */ + 'authorId': string; + /** + * + * @type {string} + * @memberof Task + */ + 'workType': string; + /** + * 音声ファイルに紐づくOption Itemの一覧(10個固定) + * @type {Array} + * @memberof Task + */ + 'optionItemList': Array; + /** + * 音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL + * @type {string} + * @memberof Task + */ + 'url': string; + /** + * 音声ファイル名 + * @type {string} + * @memberof Task + */ + 'fileName': string; + /** + * 音声ファイルの録音時間(ミリ秒の整数値) + * @type {string} + * @memberof Task + */ + 'audioDuration': string; + /** + * 音声ファイルの録音開始日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioCreatedDate': string; + /** + * 音声ファイルの録音終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioFinishedDate': string; + /** + * 音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'audioUploadedDate': string; + /** + * 音声ファイルのファイルサイズ(Byte) + * @type {number} + * @memberof Task + */ + 'fileSize': number; + /** + * 音声ファイルの優先度 \"00\":Normal / \"01\":High + * @type {string} + * @memberof Task + */ + 'priority': string; + /** + * 録音形式: DSS/DS2(SP)/DS2(QP) + * @type {string} + * @memberof Task + */ + 'audioFormat': string; + /** + * コメント + * @type {string} + * @memberof Task + */ + 'comment': string; + /** + * + * @type {boolean} + * @memberof Task + */ + 'isEncrypted': boolean; + /** + * JOBナンバー + * @type {string} + * @memberof Task + */ + 'jobNumber': string; + /** + * + * @type {Typist} + * @memberof Task + */ + 'typist'?: Typist; + /** + * 文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧 + * @type {Array} + * @memberof Task + */ + 'assignees': Array; + /** + * 音声ファイルのファイルステータス Uploaded / Pending / InProgress / Finished / Backup + * @type {string} + * @memberof Task + */ + 'status': string; + /** + * 文字起こし開始日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'transcriptionStartedDate'?: string; + /** + * 文字起こし終了日時(yyyy-mm-ddThh:mm:ss.sss) + * @type {string} + * @memberof Task + */ + 'transcriptionFinishedDate'?: string; +} +/** + * + * @export + * @interface TasksResponse + */ +export interface TasksResponse { + /** + * タスクの取得件数(指定しない場合はデフォルト値) + * @type {number} + * @memberof TasksResponse + */ + 'limit': number; + /** + * オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @type {number} + * @memberof TasksResponse + */ + 'offset': number; + /** + * タスクの総件数 + * @type {number} + * @memberof TasksResponse + */ + 'total': number; + /** + * 音声ファイル/タスク一覧 + * @type {Array} + * @memberof TasksResponse + */ + 'tasks': Array; +} +/** + * + * @export + * @interface TemplateDownloadLocationResponse + */ +export interface TemplateDownloadLocationResponse { + /** + * + * @type {string} + * @memberof TemplateDownloadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface TemplateFile + */ +export interface TemplateFile { + /** + * テンプレートファイルのID + * @type {number} + * @memberof TemplateFile + */ + 'id': number; + /** + * テンプレートファイルのファイル名 + * @type {string} + * @memberof TemplateFile + */ + 'name': string; +} +/** + * + * @export + * @interface TemplateUploadFinishedRequest + */ +export interface TemplateUploadFinishedRequest { + /** + * テンプレートファイルのファイル名 + * @type {string} + * @memberof TemplateUploadFinishedRequest + */ + 'name': string; + /** + * テンプレートファイルのアップロード先URL + * @type {string} + * @memberof TemplateUploadFinishedRequest + */ + 'url': string; +} +/** + * + * @export + * @interface TemplateUploadLocationResponse + */ +export interface TemplateUploadLocationResponse { + /** + * + * @type {string} + * @memberof TemplateUploadLocationResponse + */ + 'url': string; +} +/** + * + * @export + * @interface TermInfo + */ +export interface TermInfo { + /** + * 利用規約種別 + * @type {string} + * @memberof TermInfo + */ + 'documentType': string; + /** + * バージョン + * @type {string} + * @memberof TermInfo + */ + 'version': string; +} +/** + * + * @export + * @interface TokenRequest + */ +export interface TokenRequest { + /** + * + * @type {string} + * @memberof TokenRequest + */ + 'idToken': string; + /** + * web or mobile or desktop + * @type {string} + * @memberof TokenRequest + */ + 'type': string; +} +/** + * + * @export + * @interface TokenResponse + */ +export interface TokenResponse { + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'refreshToken': string; + /** + * + * @type {string} + * @memberof TokenResponse + */ + 'accessToken': string; +} +/** + * + * @export + * @interface Typist + */ +export interface Typist { + /** + * TypistのユーザーID + * @type {number} + * @memberof Typist + */ + 'id': number; + /** + * Typistのユーザー名 + * @type {string} + * @memberof Typist + */ + 'name': string; +} +/** + * + * @export + * @interface TypistGroup + */ +export interface TypistGroup { + /** + * TypistGroupのID + * @type {number} + * @memberof TypistGroup + */ + 'id': number; + /** + * TypistGroup名 + * @type {string} + * @memberof TypistGroup + */ + 'name': string; +} +/** + * + * @export + * @interface UpdateAcceptedVersionRequest + */ +export interface UpdateAcceptedVersionRequest { + /** + * IDトークン + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'idToken': string; + /** + * 更新バージョン(EULA) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedEULAVersion': string; + /** + * 更新バージョン(PrivacyNotice) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedPrivacyNoticeVersion': string; + /** + * 更新バージョン(DPA) + * @type {string} + * @memberof UpdateAcceptedVersionRequest + */ + 'acceptedDPAVersion'?: string; +} +/** + * + * @export + * @interface UpdateAccountInfoRequest + */ +export interface UpdateAccountInfoRequest { + /** + * 親アカウントのID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'parentAccountId'?: number; + /** + * 代行操作許可 + * @type {boolean} + * @memberof UpdateAccountInfoRequest + */ + 'delegationPermission': boolean; + /** + * プライマリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'primaryAdminUserId': number; + /** + * セカンダリ管理者ID + * @type {number} + * @memberof UpdateAccountInfoRequest + */ + 'secondryAdminUserId'?: number; +} +/** + * + * @export + * @interface UpdateFileDeleteSettingRequest + */ +export interface UpdateFileDeleteSettingRequest { + /** + * 自動ファイル削除をするかどうか + * @type {boolean} + * @memberof UpdateFileDeleteSettingRequest + */ + 'autoFileDelete': boolean; + /** + * 文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間 + * @type {number} + * @memberof UpdateFileDeleteSettingRequest + */ + 'retentionDays': number; +} +/** + * + * @export + * @interface UpdateOptionItemsRequest + */ +export interface UpdateOptionItemsRequest { + /** + * + * @type {Array} + * @memberof UpdateOptionItemsRequest + */ + 'optionItems': Array; +} +/** + * + * @export + * @interface UpdateRestrictionStatusRequest + */ +export interface UpdateRestrictionStatusRequest { + /** + * 操作対象の第五階層アカウントID + * @type {number} + * @memberof UpdateRestrictionStatusRequest + */ + 'accountId': number; + /** + * 制限をかけるかどうか(trur:制限をかける) + * @type {boolean} + * @memberof UpdateRestrictionStatusRequest + */ + 'restricted': boolean; +} +/** + * + * @export + * @interface UpdateTypistGroupRequest + */ +export interface UpdateTypistGroupRequest { + /** + * + * @type {string} + * @memberof UpdateTypistGroupRequest + */ + 'typistGroupName': string; + /** + * + * @type {Array} + * @memberof UpdateTypistGroupRequest + */ + 'typistIds': Array; +} +/** + * + * @export + * @interface UpdateWorkflowRequest + */ +export interface UpdateWorkflowRequest { + /** + * Authorの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'authorId': number; + /** + * Worktypeの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'worktypeId'?: number; + /** + * テンプレートの内部ID + * @type {number} + * @memberof UpdateWorkflowRequest + */ + 'templateId'?: number; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof UpdateWorkflowRequest + */ + 'typists': Array; +} +/** + * + * @export + * @interface UpdateWorktypesRequest + */ +export interface UpdateWorktypesRequest { + /** + * WorktypeID + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof UpdateWorktypesRequest + */ + 'description'?: string; +} +/** + * + * @export + * @interface User + */ +export interface User { + /** + * + * @type {number} + * @memberof User + */ + 'id': number; + /** + * + * @type {string} + * @memberof User + */ + 'name': string; + /** + * none/author/typist + * @type {string} + * @memberof User + */ + 'role': string; + /** + * + * @type {string} + * @memberof User + */ + 'authorId'?: string; + /** + * + * @type {Array} + * @memberof User + */ + 'typistGroupName': Array; + /** + * + * @type {string} + * @memberof User + */ + 'email': string; + /** + * + * @type {boolean} + * @memberof User + */ + 'emailVerified': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'autoRenew': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'notification': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'encryption': boolean; + /** + * + * @type {boolean} + * @memberof User + */ + 'prompt': boolean; + /** + * + * @type {string} + * @memberof User + */ + 'expiration'?: string; + /** + * + * @type {number} + * @memberof User + */ + 'remaining'?: number; + /** + * Normal/NoLicense/Alert/Renew + * @type {string} + * @memberof User + */ + 'licenseStatus': string; +} +/** + * + * @export + * @interface Workflow + */ +export interface Workflow { + /** + * ワークフローの内部ID + * @type {number} + * @memberof Workflow + */ + 'id': number; + /** + * + * @type {Author} + * @memberof Workflow + */ + 'author': Author; + /** + * + * @type {WorkflowWorktype} + * @memberof Workflow + */ + 'worktype'?: WorkflowWorktype; + /** + * + * @type {WorkflowTemplate} + * @memberof Workflow + */ + 'template'?: WorkflowTemplate; + /** + * ルーティング候補のタイピストユーザー/タイピストグループ + * @type {Array} + * @memberof Workflow + */ + 'typists': Array; +} +/** + * + * @export + * @interface WorkflowTemplate + */ +export interface WorkflowTemplate { + /** + * テンプレートの内部ID + * @type {number} + * @memberof WorkflowTemplate + */ + 'id': number; + /** + * テンプレートのファイル名 + * @type {string} + * @memberof WorkflowTemplate + */ + 'fileName': string; +} +/** + * + * @export + * @interface WorkflowTypist + */ +export interface WorkflowTypist { + /** + * タイピストユーザーの内部ID + * @type {number} + * @memberof WorkflowTypist + */ + 'typistId'?: number; + /** + * タイピストグループの内部ID + * @type {number} + * @memberof WorkflowTypist + */ + 'typistGroupId'?: number; +} +/** + * + * @export + * @interface WorkflowWorktype + */ +export interface WorkflowWorktype { + /** + * Worktypeの内部ID + * @type {number} + * @memberof WorkflowWorktype + */ + 'id': number; + /** + * WorktypeID + * @type {string} + * @memberof WorkflowWorktype + */ + 'worktypeId': string; +} +/** + * + * @export + * @interface Worktype + */ +export interface Worktype { + /** + * WorktypeのID + * @type {number} + * @memberof Worktype + */ + 'id': number; + /** + * WorktypeID + * @type {string} + * @memberof Worktype + */ + 'worktypeId': string; + /** + * Worktypeの説明 + * @type {string} + * @memberof Worktype + */ + 'description'?: string; +} + +/** + * AccountsApi - axios parameter creator + * @export + */ +export const AccountsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activeWorktype: async (postActiveWorktypeRequest: PostActiveWorktypeRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postActiveWorktypeRequest' is not null or undefined + assertParamExists('activeWorktype', 'postActiveWorktypeRequest', postActiveWorktypeRequest) + const localVarPath = `/accounts/active-worktype`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postActiveWorktypeRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelIssue: async (cancelIssueRequest: CancelIssueRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'cancelIssueRequest' is not null or undefined + assertParamExists('cancelIssue', 'cancelIssueRequest', cancelIssueRequest) + const localVarPath = `/accounts/issue/cancel`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(cancelIssueRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAccount: async (createAccountRequest: CreateAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createAccountRequest' is not null or undefined + assertParamExists('createAccount', 'createAccountRequest', createAccountRequest) + const localVarPath = `/accounts`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPartnerAccount: async (createPartnerAccountRequest: CreatePartnerAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createPartnerAccountRequest' is not null or undefined + assertParamExists('createPartnerAccount', 'createPartnerAccountRequest', createPartnerAccountRequest) + const localVarPath = `/accounts/partner`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createPartnerAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createTypistGroup: async (createTypistGroupRequest: CreateTypistGroupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createTypistGroupRequest' is not null or undefined + assertParamExists('createTypistGroup', 'createTypistGroupRequest', createTypistGroupRequest) + const localVarPath = `/accounts/typist-groups`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createTypistGroupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorktype: async (createWorktypesRequest: CreateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createWorktypesRequest' is not null or undefined + assertParamExists('createWorktype', 'createWorktypesRequest', createWorktypesRequest) + const localVarPath = `/accounts/worktypes`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createWorktypesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteAccountAndData: async (deleteAccountRequest: DeleteAccountRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deleteAccountRequest' is not null or undefined + assertParamExists('deleteAccountAndData', 'deleteAccountRequest', deleteAccountRequest) + const localVarPath = `/accounts/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deleteAccountRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('deleteTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}/delete` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteWorktype', 'id', id) + const localVarPath = `/accounts/worktypes/{id}/delete` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAccountInfoMinimalAccess: async (getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getAccountInfoMinimalAccessRequest' is not null or undefined + assertParamExists('getAccountInfoMinimalAccess', 'getAccountInfoMinimalAccessRequest', getAccountInfoMinimalAccessRequest) + const localVarPath = `/accounts/minimal-access`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getAccountInfoMinimalAccessRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuthors: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/authors`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName: async (getCompanyNameRequest: GetCompanyNameRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getCompanyNameRequest' is not null or undefined + assertParamExists('getCompanyName', 'getCompanyNameRequest', getCompanyNameRequest) + const localVarPath = `/accounts/company-name`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getCompanyNameRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDealers: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/dealers`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getLicenseSummary: async (getLicenseSummaryRequest: GetLicenseSummaryRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getLicenseSummaryRequest' is not null or undefined + assertParamExists('getLicenseSummary', 'getLicenseSummaryRequest', getLicenseSummaryRequest) + const localVarPath = `/accounts/licenses/summary`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getLicenseSummaryRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyAccount: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOptionItems: async (id: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('getOptionItems', 'id', id) + const localVarPath = `/accounts/worktypes/{id}/option-items` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOrderHistories: async (getOrderHistoriesRequest: GetOrderHistoriesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getOrderHistoriesRequest' is not null or undefined + assertParamExists('getOrderHistories', 'getOrderHistoriesRequest', getOrderHistoriesRequest) + const localVarPath = `/accounts/order-histories`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getOrderHistoriesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerLicenses: async (getPartnerLicensesRequest: GetPartnerLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'getPartnerLicensesRequest' is not null or undefined + assertParamExists('getPartnerLicenses', 'getPartnerLicensesRequest', getPartnerLicensesRequest) + const localVarPath = `/accounts/partner-licenses`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(getPartnerLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners: async (limit: number, offset: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'limit' is not null or undefined + assertParamExists('getPartners', 'limit', limit) + // verify required parameter 'offset' is not null or undefined + assertParamExists('getPartners', 'offset', offset) + const localVarPath = `/accounts/partners`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroup: async (typistGroupId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('getTypistGroup', 'typistGroupId', typistGroupId) + const localVarPath = `/accounts/typist-groups/{typistGroupId}` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroups: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/typist-groups`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypists: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/typists`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorktypes: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/accounts/worktypes`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueLicense: async (issueLicenseRequest: IssueLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'issueLicenseRequest' is not null or undefined + assertParamExists('issueLicense', 'issueLicenseRequest', issueLicenseRequest) + const localVarPath = `/accounts/licenses/issue`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(issueLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAccountInfo: async (updateAccountInfoRequest: UpdateAccountInfoRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateAccountInfoRequest' is not null or undefined + assertParamExists('updateAccountInfo', 'updateAccountInfoRequest', updateAccountInfoRequest) + const localVarPath = `/accounts/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateAccountInfoRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting: async (updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateFileDeleteSettingRequest' is not null or undefined + assertParamExists('updateFileDeleteSetting', 'updateFileDeleteSettingRequest', updateFileDeleteSettingRequest) + const localVarPath = `/accounts/me/file-delete-setting`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateFileDeleteSettingRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateOptionItems: async (id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateOptionItems', 'id', id) + // verify required parameter 'updateOptionItemsRequest' is not null or undefined + assertParamExists('updateOptionItems', 'updateOptionItemsRequest', updateOptionItemsRequest) + const localVarPath = `/accounts/worktypes/{id}/option-items` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateOptionItemsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus: async (updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateRestrictionStatusRequest' is not null or undefined + assertParamExists('updateRestrictionStatus', 'updateRestrictionStatusRequest', updateRestrictionStatusRequest) + const localVarPath = `/accounts/restriction-status`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateRestrictionStatusRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateTypistGroup: async (typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'typistGroupId' is not null or undefined + assertParamExists('updateTypistGroup', 'typistGroupId', typistGroupId) + // verify required parameter 'updateTypistGroupRequest' is not null or undefined + assertParamExists('updateTypistGroup', 'updateTypistGroupRequest', updateTypistGroupRequest) + const localVarPath = `/accounts/typist-groups/{typistGroupId}` + .replace(`{${"typistGroupId"}}`, encodeURIComponent(String(typistGroupId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateTypistGroupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype: async (id: number, updateWorktypesRequest: UpdateWorktypesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateWorktype', 'id', id) + // verify required parameter 'updateWorktypesRequest' is not null or undefined + assertParamExists('updateWorktype', 'updateWorktypesRequest', updateWorktypesRequest) + const localVarPath = `/accounts/worktypes/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateWorktypesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AccountsApi - functional programming interface + * @export + */ +export const AccountsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AccountsApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.activeWorktype(postActiveWorktypeRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.activeWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelIssue(cancelIssueRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.cancelIssue']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createAccount(createAccountRequest: CreateAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createAccount(createAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createPartnerAccount(createPartnerAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createPartnerAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createTypistGroup(createTypistGroupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createWorktype(createWorktypesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.createWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAccountAndData(deleteAccountRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteAccountAndData']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteWorktype(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorktype(id, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.deleteWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getAccountInfoMinimalAccess']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAuthors(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAuthors(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getAuthors']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getCompanyName(getCompanyNameRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getCompanyName']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getDealers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getDealers(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getDealers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getLicenseSummary(getLicenseSummaryRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getLicenseSummary']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMyAccount(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMyAccount(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getMyAccount']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOptionItems(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOptionItems(id, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getOptionItems']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOrderHistories(getOrderHistoriesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getOrderHistories']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartnerLicenses(getPartnerLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartnerLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPartners(limit: number, offset: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(limit, offset, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getPartners']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypistGroup(typistGroupId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypistGroup(typistGroupId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypistGroups(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypistGroups(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypistGroups']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTypists(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTypists(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getTypists']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getWorktypes(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getWorktypes(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.getWorktypes']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.issueLicense(issueLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.issueLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateAccountInfo(updateAccountInfoRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateAccountInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateFileDeleteSetting(updateFileDeleteSettingRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateFileDeleteSetting']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateOptionItems(id, updateOptionItemsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateOptionItems']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateRestrictionStatus(updateRestrictionStatusRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateRestrictionStatus']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateTypistGroup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorktype(id, updateWorktypesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AccountsApi.updateWorktype']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * AccountsApi - factory interface + * @export + */ +export const AccountsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AccountsApiFp(configuration) + return { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: any): AxiosPromise { + return localVarFp.activeWorktype(postActiveWorktypeRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: any): AxiosPromise { + return localVarFp.cancelIssue(cancelIssueRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAccount(createAccountRequest: CreateAccountRequest, options?: any): AxiosPromise { + return localVarFp.createAccount(createAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: any): AxiosPromise { + return localVarFp.createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: any): AxiosPromise { + return localVarFp.createTypistGroup(createTypistGroupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.createWorktype(createWorktypesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: any): AxiosPromise { + return localVarFp.deleteAccountAndData(deleteAccountRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.deleteTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorktype(id: number, options?: any): AxiosPromise { + return localVarFp.deleteWorktype(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: any): AxiosPromise { + return localVarFp.getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAuthors(options?: any): AxiosPromise { + return localVarFp.getAuthors(options).then((request) => request(axios, basePath)); + }, + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: any): AxiosPromise { + return localVarFp.getCompanyName(getCompanyNameRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getDealers(options?: any): AxiosPromise { + return localVarFp.getDealers(options).then((request) => request(axios, basePath)); + }, + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: any): AxiosPromise { + return localVarFp.getLicenseSummary(getLicenseSummaryRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyAccount(options?: any): AxiosPromise { + return localVarFp.getMyAccount(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOptionItems(id: number, options?: any): AxiosPromise { + return localVarFp.getOptionItems(id, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: any): AxiosPromise { + return localVarFp.getOrderHistories(getOrderHistoriesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: any): AxiosPromise { + return localVarFp.getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPartners(limit: number, offset: number, options?: any): AxiosPromise { + return localVarFp.getPartners(limit, offset, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroup(typistGroupId: number, options?: any): AxiosPromise { + return localVarFp.getTypistGroup(typistGroupId, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypistGroups(options?: any): AxiosPromise { + return localVarFp.getTypistGroups(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTypists(options?: any): AxiosPromise { + return localVarFp.getTypists(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorktypes(options?: any): AxiosPromise { + return localVarFp.getWorktypes(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: any): AxiosPromise { + return localVarFp.issueLicense(issueLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: any): AxiosPromise { + return localVarFp.updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: any): AxiosPromise { + return localVarFp.updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: any): AxiosPromise { + return localVarFp.updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: any): AxiosPromise { + return localVarFp.updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: any): AxiosPromise { + return localVarFp.updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: any): AxiosPromise { + return localVarFp.updateWorktype(id, updateWorktypesRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AccountsApi - object-oriented interface + * @export + * @class AccountsApi + * @extends {BaseAPI} + */ +export class AccountsApi extends BaseAPI { + /** + * + * @summary + * @param {PostActiveWorktypeRequest} postActiveWorktypeRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public activeWorktype(postActiveWorktypeRequest: PostActiveWorktypeRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).activeWorktype(postActiveWorktypeRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス発行をキャンセルします + * @summary + * @param {CancelIssueRequest} cancelIssueRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public cancelIssue(cancelIssueRequest: CancelIssueRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).cancelIssue(cancelIssueRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateAccountRequest} createAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createAccount(createAccountRequest: CreateAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createAccount(createAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreatePartnerAccountRequest} createPartnerAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createPartnerAccount(createPartnerAccountRequest: CreatePartnerAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createPartnerAccount(createPartnerAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下にタイピストグループを追加します + * @summary + * @param {CreateTypistGroupRequest} createTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createTypistGroup(createTypistGroupRequest: CreateTypistGroupRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createTypistGroup(createTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateWorktypesRequest} createWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public createWorktype(createWorktypesRequest: CreateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).createWorktype(createWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {DeleteAccountRequest} deleteAccountRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteAccountAndData(deleteAccountRequest: DeleteAccountRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteAccountAndData(deleteAccountRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public deleteWorktype(id: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).deleteWorktype(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetAccountInfoMinimalAccessRequest} getAccountInfoMinimalAccessRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest: GetAccountInfoMinimalAccessRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getAccountInfoMinimalAccess(getAccountInfoMinimalAccessRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のAuthor一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getAuthors(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getAuthors(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定したアカウントの会社名を取得します + * @summary + * @param {GetCompanyNameRequest} getCompanyNameRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getCompanyName(getCompanyNameRequest: GetCompanyNameRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getCompanyName(getCompanyNameRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getDealers(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getDealers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定したアカウントのライセンス集計情報を取得します + * @summary + * @param {GetLicenseSummaryRequest} getLicenseSummaryRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getLicenseSummary(getLicenseSummaryRequest: GetLicenseSummaryRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getLicenseSummary(getLicenseSummaryRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getMyAccount(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getMyAccount(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getOptionItems(id: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getOptionItems(id, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetOrderHistoriesRequest} getOrderHistoriesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getOrderHistories(getOrderHistoriesRequest: GetOrderHistoriesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getOrderHistories(getOrderHistoriesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {GetPartnerLicensesRequest} getPartnerLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartnerLicenses(getPartnerLicensesRequest: GetPartnerLicensesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartnerLicenses(getPartnerLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} limit 取得件数 + * @param {number} offset 開始位置 + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getPartners(limit: number, offset: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getPartners(limit, offset, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します + * @summary + * @param {number} typistGroupId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypistGroup(typistGroupId: number, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypistGroup(typistGroupId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypistGroups(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypistGroups(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下のタイピスト一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getTypists(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getTypists(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public getWorktypes(options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).getWorktypes(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {IssueLicenseRequest} issueLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public issueLicense(issueLicenseRequest: IssueLicenseRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).issueLicense(issueLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateAccountInfoRequest} updateAccountInfoRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateAccountInfo(updateAccountInfoRequest: UpdateAccountInfoRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateAccountInfo(updateAccountInfoRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateFileDeleteSettingRequest} updateFileDeleteSettingRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateFileDeleteSetting(updateFileDeleteSettingRequest: UpdateFileDeleteSettingRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateFileDeleteSetting(updateFileDeleteSettingRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateOptionItemsRequest} updateOptionItemsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateOptionItems(id: number, updateOptionItemsRequest: UpdateOptionItemsRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateOptionItems(id, updateOptionItemsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {UpdateRestrictionStatusRequest} updateRestrictionStatusRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateRestrictionStatus(updateRestrictionStatusRequest: UpdateRestrictionStatusRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateRestrictionStatus(updateRestrictionStatusRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します + * @summary + * @param {number} typistGroupId + * @param {UpdateTypistGroupRequest} updateTypistGroupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateTypistGroup(typistGroupId: number, updateTypistGroupRequest: UpdateTypistGroupRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateTypistGroup(typistGroupId, updateTypistGroupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {number} id Worktypeの内部ID + * @param {UpdateWorktypesRequest} updateWorktypesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AccountsApi + */ + public updateWorktype(id: number, updateWorktypesRequest: UpdateWorktypesRequest, options?: AxiosRequestConfig) { + return AccountsApiFp(this.configuration).updateWorktype(id, updateWorktypesRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * AuthApi - axios parameter creator + * @export + */ +export const AuthApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/accessToken`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationAccessToken: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/auth/delegation/access-token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationToken: async (delegationTokenRequest: DelegationTokenRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'delegationTokenRequest' is not null or undefined + assertParamExists('delegationToken', 'delegationTokenRequest', delegationTokenRequest) + const localVarPath = `/auth/delegation/token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(delegationTokenRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token: async (tokenRequest: TokenRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'tokenRequest' is not null or undefined + assertParamExists('token', 'tokenRequest', tokenRequest) + const localVarPath = `/auth/token`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(tokenRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * AuthApi - functional programming interface + * @export + */ +export const AuthApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = AuthApiAxiosParamCreator(configuration) + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async accessToken(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.accessToken(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.accessToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async delegationAccessToken(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.delegationAccessToken(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.delegationAccessToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.delegationToken(delegationTokenRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.delegationToken']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async token(tokenRequest: TokenRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.token(tokenRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['AuthApi.token']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * AuthApi - factory interface + * @export + */ +export const AuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = AuthApiFp(configuration) + return { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + accessToken(options?: any): AxiosPromise { + return localVarFp.accessToken(options).then((request) => request(axios, basePath)); + }, + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationAccessToken(options?: any): AxiosPromise { + return localVarFp.delegationAccessToken(options).then((request) => request(axios, basePath)); + }, + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: any): AxiosPromise { + return localVarFp.delegationToken(delegationTokenRequest, options).then((request) => request(axios, basePath)); + }, + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + token(tokenRequest: TokenRequest, options?: any): AxiosPromise { + return localVarFp.token(tokenRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * AuthApi - object-oriented interface + * @export + * @class AuthApi + * @extends {BaseAPI} + */ +export class AuthApi extends BaseAPI { + /** + * リフレッシュトークンを元にアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public accessToken(options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).accessToken(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 代行操作用のアクセストークンを再生成します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public delegationAccessToken(options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).delegationAccessToken(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 代行操作用のリフレッシュトークン・アクセストークンを生成します + * @summary + * @param {DelegationTokenRequest} delegationTokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public delegationToken(delegationTokenRequest: DelegationTokenRequest, options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).delegationToken(delegationTokenRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します + * @summary + * @param {TokenRequest} tokenRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AuthApi + */ + public token(tokenRequest: TokenRequest, options?: AxiosRequestConfig) { + return AuthApiFp(this.configuration).token(tokenRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/health`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkHealth(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkHealth(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['DefaultApi.checkHealth']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkHealth(options?: any): AxiosPromise { + return localVarFp.checkHealth(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public checkHealth(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).checkHealth(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * FilesApi - axios parameter creator + * @export + */ +export const FilesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('downloadLocation', 'audioFileId', audioFileId) + const localVarPath = `/files/audio/download-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (audioFileId !== undefined) { + localVarQueryParameter['audioFileId'] = audioFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadTemplateLocation: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('downloadTemplateLocation', 'audioFileId', audioFileId) + const localVarPath = `/files/template/download-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (audioFileId !== undefined) { + localVarQueryParameter['audioFileId'] = audioFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadFinished: async (audioUploadFinishedRequest: AudioUploadFinishedRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioUploadFinishedRequest' is not null or undefined + assertParamExists('uploadFinished', 'audioUploadFinishedRequest', audioUploadFinishedRequest) + const localVarPath = `/files/audio/upload-finished`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(audioUploadFinishedRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/files/audio/upload-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateFinished: async (templateUploadFinishedRequest: TemplateUploadFinishedRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateUploadFinishedRequest' is not null or undefined + assertParamExists('uploadTemplateFinished', 'templateUploadFinishedRequest', templateUploadFinishedRequest) + const localVarPath = `/files/template/upload-finished`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(templateUploadFinishedRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateLocation: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/files/template/upload-location`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * FilesApi - functional programming interface + * @export + */ +export const FilesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = FilesApiAxiosParamCreator(configuration) + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async downloadLocation(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadLocation(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.downloadLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async downloadTemplateLocation(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.downloadTemplateLocation(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.downloadTemplateLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFinished(audioUploadFinishedRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadFinished']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadLocation(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadLocation(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadTemplateFinished(templateUploadFinishedRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadTemplateFinished']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async uploadTemplateLocation(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadTemplateLocation(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['FilesApi.uploadTemplateLocation']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * FilesApi - factory interface + * @export + */ +export const FilesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = FilesApiFp(configuration) + return { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadLocation(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.downloadLocation(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + downloadTemplateLocation(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.downloadTemplateLocation(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: any): AxiosPromise { + return localVarFp.uploadFinished(audioUploadFinishedRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadLocation(options?: any): AxiosPromise { + return localVarFp.uploadLocation(options).then((request) => request(axios, basePath)); + }, + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: any): AxiosPromise { + return localVarFp.uploadTemplateFinished(templateUploadFinishedRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + uploadTemplateLocation(options?: any): AxiosPromise { + return localVarFp.uploadTemplateLocation(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * FilesApi - object-oriented interface + * @export + * @class FilesApi + * @extends {BaseAPI} + */ +export class FilesApi extends BaseAPI { + /** + * 指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId ODMSCloud上で管理する音声ファイルのID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public downloadLocation(audioFileId: number, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).downloadLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します + * @summary + * @param {number} audioFileId 文字起こし対象の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public downloadTemplateLocation(audioFileId: number, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).downloadTemplateLocation(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します + * @summary + * @param {AudioUploadFinishedRequest} audioUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadFinished(audioUploadFinishedRequest: AudioUploadFinishedRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadFinished(audioUploadFinishedRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadLocation(options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadLocation(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アップロードが完了したテンプレートファイルの情報を登録します + * @summary + * @param {TemplateUploadFinishedRequest} templateUploadFinishedRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadTemplateFinished(templateUploadFinishedRequest: TemplateUploadFinishedRequest, options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadTemplateFinished(templateUploadFinishedRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof FilesApi + */ + public uploadTemplateLocation(options?: AxiosRequestConfig) { + return FilesApiFp(this.configuration).uploadTemplateLocation(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * LicensesApi - axios parameter creator + * @export + */ +export const LicensesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activateCardLicenses: async (activateCardLicensesRequest: ActivateCardLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'activateCardLicensesRequest' is not null or undefined + assertParamExists('activateCardLicenses', 'activateCardLicensesRequest', activateCardLicensesRequest) + const localVarPath = `/licenses/cards/activate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(activateCardLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelOrder: async (cancelOrderRequest: CancelOrderRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'cancelOrderRequest' is not null or undefined + assertParamExists('cancelOrder', 'cancelOrderRequest', cancelOrderRequest) + const localVarPath = `/licenses/orders/cancel`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(cancelOrderRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createOrders: async (createOrdersRequest: CreateOrdersRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createOrdersRequest' is not null or undefined + assertParamExists('createOrders', 'createOrdersRequest', createOrdersRequest) + const localVarPath = `/licenses/orders`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createOrdersRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllocatableLicenses: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/licenses/allocatable`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCardLicenses: async (issueCardLicensesRequest: IssueCardLicensesRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'issueCardLicensesRequest' is not null or undefined + assertParamExists('issueCardLicenses', 'issueCardLicensesRequest', issueCardLicensesRequest) + const localVarPath = `/licenses/cards`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(issueCardLicensesRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * LicensesApi - functional programming interface + * @export + */ +export const LicensesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = LicensesApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.activateCardLicenses(activateCardLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.activateCardLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancelOrder(cancelOrderRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.cancelOrder']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createOrders(createOrdersRequest: CreateOrdersRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createOrders(createOrdersRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.createOrders']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllocatableLicenses(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllocatableLicenses(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.getAllocatableLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.issueCardLicenses(issueCardLicensesRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['LicensesApi.issueCardLicenses']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * LicensesApi - factory interface + * @export + */ +export const LicensesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = LicensesApiFp(configuration) + return { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: any): AxiosPromise { + return localVarFp.activateCardLicenses(activateCardLicensesRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: any): AxiosPromise { + return localVarFp.cancelOrder(cancelOrderRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createOrders(createOrdersRequest: CreateOrdersRequest, options?: any): AxiosPromise { + return localVarFp.createOrders(createOrdersRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllocatableLicenses(options?: any): AxiosPromise { + return localVarFp.getAllocatableLicenses(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: any): AxiosPromise { + return localVarFp.issueCardLicenses(issueCardLicensesRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * LicensesApi - object-oriented interface + * @export + * @class LicensesApi + * @extends {BaseAPI} + */ +export class LicensesApi extends BaseAPI { + /** + * + * @summary + * @param {ActivateCardLicensesRequest} activateCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public activateCardLicenses(activateCardLicensesRequest: ActivateCardLicensesRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).activateCardLicenses(activateCardLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス注文をキャンセルします + * @summary + * @param {CancelOrderRequest} cancelOrderRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public cancelOrder(cancelOrderRequest: CancelOrderRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).cancelOrder(cancelOrderRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {CreateOrdersRequest} createOrdersRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public createOrders(createOrdersRequest: CreateOrdersRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).createOrders(createOrdersRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 割り当て可能なライセンスを取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public getAllocatableLicenses(options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).getAllocatableLicenses(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {IssueCardLicensesRequest} issueCardLicensesRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof LicensesApi + */ + public issueCardLicenses(issueCardLicensesRequest: IssueCardLicensesRequest, options?: AxiosRequestConfig) { + return LicensesApiFp(this.configuration).issueCardLicenses(issueCardLicensesRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * NotificationApi - axios parameter creator + * @export + */ +export const NotificationApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + register: async (registerRequest: RegisterRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'registerRequest' is not null or undefined + assertParamExists('register', 'registerRequest', registerRequest) + const localVarPath = `/notification/register`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(registerRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * NotificationApi - functional programming interface + * @export + */ +export const NotificationApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = NotificationApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async register(registerRequest: RegisterRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.register(registerRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['NotificationApi.register']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * NotificationApi - factory interface + * @export + */ +export const NotificationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = NotificationApiFp(configuration) + return { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + register(registerRequest: RegisterRequest, options?: any): AxiosPromise { + return localVarFp.register(registerRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * NotificationApi - object-oriented interface + * @export + * @class NotificationApi + * @extends {BaseAPI} + */ +export class NotificationApi extends BaseAPI { + /** + * + * @summary + * @param {RegisterRequest} registerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof NotificationApi + */ + public register(registerRequest: RegisterRequest, options?: AxiosRequestConfig) { + return NotificationApiFp(this.configuration).register(registerRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TasksApi - axios parameter creator + * @export + */ +export const TasksApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + backup: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('backup', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/backup` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancel: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('cancel', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/cancel` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + changeCheckoutPermission: async (audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('changeCheckoutPermission', 'audioFileId', audioFileId) + // verify required parameter 'postCheckoutPermissionRequest' is not null or undefined + assertParamExists('changeCheckoutPermission', 'postCheckoutPermissionRequest', postCheckoutPermissionRequest) + const localVarPath = `/tasks/{audioFileId}/checkout-permission` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postCheckoutPermissionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkin: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('checkin', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/checkin` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkout: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('checkout', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/checkout` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('deleteTask', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/delete` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNextAudioFile: async (endedFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'endedFileId' is not null or undefined + assertParamExists('getNextAudioFile', 'endedFileId', endedFileId) + const localVarPath = `/tasks/next`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (endedFileId !== undefined) { + localVarQueryParameter['endedFileId'] = endedFileId; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTasks: async (limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/tasks`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (limit !== undefined) { + localVarQueryParameter['limit'] = limit; + } + + if (offset !== undefined) { + localVarQueryParameter['offset'] = offset; + } + + if (status !== undefined) { + localVarQueryParameter['status'] = status; + } + + if (direction !== undefined) { + localVarQueryParameter['direction'] = direction; + } + + if (paramName !== undefined) { + localVarQueryParameter['paramName'] = paramName; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + suspend: async (audioFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'audioFileId' is not null or undefined + assertParamExists('suspend', 'audioFileId', audioFileId) + const localVarPath = `/tasks/{audioFileId}/suspend` + .replace(`{${"audioFileId"}}`, encodeURIComponent(String(audioFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TasksApi - functional programming interface + * @export + */ +export const TasksApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration) + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async backup(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.backup(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.backup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async cancel(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.cancel(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.cancel']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.changeCheckoutPermission']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkin(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkin(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.checkin']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async checkout(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.checkout(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.checkout']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTask(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTask(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.deleteTask']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getNextAudioFile(endedFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getNextAudioFile(endedFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.getNextAudioFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTasks(limit, offset, status, direction, paramName, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.getTasks']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async suspend(audioFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.suspend(audioFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TasksApi.suspend']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TasksApi - factory interface + * @export + */ +export const TasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TasksApiFp(configuration) + return { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + backup(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.backup(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + cancel(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.cancel(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: any): AxiosPromise { + return localVarFp.changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkin(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.checkin(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + checkout(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.checkout(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTask(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTask(audioFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getNextAudioFile(endedFileId: number, options?: any): AxiosPromise { + return localVarFp.getNextAudioFile(endedFileId, options).then((request) => request(axios, basePath)); + }, + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: any): AxiosPromise { + return localVarFp.getTasks(limit, offset, status, direction, paramName, options).then((request) => request(axios, basePath)); + }, + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + suspend(audioFileId: number, options?: any): AxiosPromise { + return localVarFp.suspend(audioFileId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TasksApi - object-oriented interface + * @export + * @class TasksApi + * @extends {BaseAPI} + */ +export class TasksApi extends BaseAPI { + /** + * 指定した文字起こしタスクをバックアップします(ステータスをBackupにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public backup(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).backup(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public cancel(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).cancel(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクのチェックアウト候補を変更します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {PostCheckoutPermissionRequest} postCheckoutPermissionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public changeCheckoutPermission(audioFileId: number, postCheckoutPermissionRequest: PostCheckoutPermissionRequest, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).changeCheckoutPermission(audioFileId, postCheckoutPermissionRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをチェックインします(ステータスをFinishedにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public checkin(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).checkin(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public checkout(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).checkout(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクを削除します。 + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public deleteTask(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).deleteTask(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します + * @summary + * @param {number} endedFileId 文字起こし完了したタスクの音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public getNextAudioFile(endedFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).getNextAudioFile(endedFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 音声ファイル・文字起こしタスク情報をページ指定して取得します + * @summary + * @param {number} [limit] タスクの取得件数(指定しない場合はデフォルト値) + * @param {number} [offset] オフセット(何件目から取得するか 設定しない場合はデフォルト値) + * @param {string} [status] 取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup + * @param {string} [direction] ASC/DESC + * @param {string} [paramName] JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public getTasks(limit?: number, offset?: number, status?: string, direction?: string, paramName?: string, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).getTasks(limit, offset, status, direction, paramName, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 指定した文字起こしタスクを一時中断します(ステータスをPendingにします) + * @summary + * @param {number} audioFileId ODMS Cloud上の音声ファイルID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public suspend(audioFileId: number, options?: AxiosRequestConfig) { + return TasksApiFp(this.configuration).suspend(audioFileId, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TemplatesApi - axios parameter creator + * @export + */ +export const TemplatesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile: async (templateFileId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'templateFileId' is not null or undefined + assertParamExists('deleteTemplateFile', 'templateFileId', templateFileId) + const localVarPath = `/templates/{templateFileId}/delete` + .replace(`{${"templateFileId"}}`, encodeURIComponent(String(templateFileId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTemplates: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/templates`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TemplatesApi - functional programming interface + * @export + */ +export const TemplatesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TemplatesApiAxiosParamCreator(configuration) + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTemplateFile(templateFileId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.deleteTemplateFile']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTemplates(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTemplates(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TemplatesApi.getTemplates']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TemplatesApi - factory interface + * @export + */ +export const TemplatesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TemplatesApiFp(configuration) + return { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteTemplateFile(templateFileId: number, options?: any): AxiosPromise { + return localVarFp.deleteTemplateFile(templateFileId, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTemplates(options?: any): AxiosPromise { + return localVarFp.getTemplates(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TemplatesApi - object-oriented interface + * @export + * @class TemplatesApi + * @extends {BaseAPI} + */ +export class TemplatesApi extends BaseAPI { + /** + * ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します + * @summary + * @param {number} templateFileId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public deleteTemplateFile(templateFileId: number, options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).deleteTemplateFile(templateFileId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のテンプレートファイルの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TemplatesApi + */ + public getTemplates(options?: AxiosRequestConfig) { + return TemplatesApiFp(this.configuration).getTemplates(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * TermsApi - axios parameter creator + * @export + */ +export const TermsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTermsInfo: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/terms`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * TermsApi - functional programming interface + * @export + */ +export const TermsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = TermsApiAxiosParamCreator(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getTermsInfo(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTermsInfo(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['TermsApi.getTermsInfo']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * TermsApi - factory interface + * @export + */ +export const TermsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = TermsApiFp(configuration) + return { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getTermsInfo(options?: any): AxiosPromise { + return localVarFp.getTermsInfo(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * TermsApi - object-oriented interface + * @export + * @class TermsApi + * @extends {BaseAPI} + */ +export class TermsApi extends BaseAPI { + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TermsApi + */ + public getTermsInfo(options?: AxiosRequestConfig) { + return TermsApiFp(this.configuration).getTermsInfo(options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * UsersApi - axios parameter creator + * @export + */ +export const UsersApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + allocateLicense: async (allocateLicenseRequest: AllocateLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'allocateLicenseRequest' is not null or undefined + assertParamExists('allocateLicense', 'allocateLicenseRequest', allocateLicenseRequest) + const localVarPath = `/users/license/allocate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(allocateLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUser: async (confirmRequest: ConfirmRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'confirmRequest' is not null or undefined + assertParamExists('confirmUser', 'confirmRequest', confirmRequest) + const localVarPath = `/users/confirm`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword: async (confirmRequest: ConfirmRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'confirmRequest' is not null or undefined + assertParamExists('confirmUserAndInitPassword', 'confirmRequest', confirmRequest) + const localVarPath = `/users/confirm/initpassword`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(confirmRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deallocateLicense: async (deallocateLicenseRequest: DeallocateLicenseRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deallocateLicenseRequest' is not null or undefined + assertParamExists('deallocateLicense', 'deallocateLicenseRequest', deallocateLicenseRequest) + const localVarPath = `/users/license/deallocate`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(deallocateLicenseRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser: async (postDeleteUserRequest: PostDeleteUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postDeleteUserRequest' is not null or undefined + assertParamExists('deleteUser', 'postDeleteUserRequest', postDeleteUserRequest) + const localVarPath = `/users/delete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postDeleteUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyUser: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/me`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getRelations: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/relations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSortCriteria: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users/sort-criteria`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/users`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports: async (postMultipleImportsRequest: PostMultipleImportsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsRequest' is not null or undefined + assertParamExists('multipleImports', 'postMultipleImportsRequest', postMultipleImportsRequest) + const localVarPath = `/users/multiple-imports`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate: async (postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postMultipleImportsCompleteRequest' is not null or undefined + assertParamExists('multipleImportsComplate', 'postMultipleImportsCompleteRequest', postMultipleImportsCompleteRequest) + const localVarPath = `/users/multiple-imports/complete`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postMultipleImportsCompleteRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup: async (signupRequest: SignupRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'signupRequest' is not null or undefined + assertParamExists('signup', 'signupRequest', signupRequest) + const localVarPath = `/users/signup`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(signupRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAcceptedVersion: async (updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'updateAcceptedVersionRequest' is not null or undefined + assertParamExists('updateAcceptedVersion', 'updateAcceptedVersionRequest', updateAcceptedVersionRequest) + const localVarPath = `/users/accepted-version`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateAcceptedVersionRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSortCriteria: async (postSortCriteriaRequest: PostSortCriteriaRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postSortCriteriaRequest' is not null or undefined + assertParamExists('updateSortCriteria', 'postSortCriteriaRequest', postSortCriteriaRequest) + const localVarPath = `/users/sort-criteria`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postSortCriteriaRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateUser: async (postUpdateUserRequest: PostUpdateUserRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'postUpdateUserRequest' is not null or undefined + assertParamExists('updateUser', 'postUpdateUserRequest', postUpdateUserRequest) + const localVarPath = `/users/update`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(postUpdateUserRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * UsersApi - functional programming interface + * @export + */ +export const UsersApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration) + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.allocateLicense(allocateLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.allocateLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async confirmUser(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUser(confirmRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.confirmUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.confirmUserAndInitPassword(confirmRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.confirmUserAndInitPassword']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deallocateLicense(deallocateLicenseRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deallocateLicense']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUser(postDeleteUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.deleteUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMyUser(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMyUser(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getMyUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getRelations(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getRelations(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getRelations']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getSortCriteria(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getSortCriteria(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getSortCriteria']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getUsers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUsers(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.getUsers']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImports(postMultipleImportsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImports']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.multipleImportsComplate(postMultipleImportsCompleteRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.multipleImportsComplate']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async signup(signupRequest: SignupRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.signup(signupRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.signup']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateAcceptedVersion(updateAcceptedVersionRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateAcceptedVersion']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateSortCriteria(postSortCriteriaRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateSortCriteria']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(postUpdateUserRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['UsersApi.updateUser']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * UsersApi - factory interface + * @export + */ +export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = UsersApiFp(configuration) + return { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: any): AxiosPromise { + return localVarFp.allocateLicense(allocateLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUser(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { + return localVarFp.confirmUser(confirmRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: any): AxiosPromise { + return localVarFp.confirmUserAndInitPassword(confirmRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: any): AxiosPromise { + return localVarFp.deallocateLicense(deallocateLicenseRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: any): AxiosPromise { + return localVarFp.deleteUser(postDeleteUserRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMyUser(options?: any): AxiosPromise { + return localVarFp.getMyUser(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getRelations(options?: any): AxiosPromise { + return localVarFp.getRelations(options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getSortCriteria(options?: any): AxiosPromise { + return localVarFp.getSortCriteria(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getUsers(options?: any): AxiosPromise { + return localVarFp.getUsers(options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: any): AxiosPromise { + return localVarFp.multipleImports(postMultipleImportsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: any): AxiosPromise { + return localVarFp.multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + signup(signupRequest: SignupRequest, options?: any): AxiosPromise { + return localVarFp.signup(signupRequest, options).then((request) => request(axios, basePath)); + }, + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: any): AxiosPromise { + return localVarFp.updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: any): AxiosPromise { + return localVarFp.updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(axios, basePath)); + }, + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: any): AxiosPromise { + return localVarFp.updateUser(postUpdateUserRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * UsersApi - object-oriented interface + * @export + * @class UsersApi + * @extends {BaseAPI} + */ +export class UsersApi extends BaseAPI { + /** + * ライセンスを割り当てます + * @summary + * @param {AllocateLicenseRequest} allocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public allocateLicense(allocateLicenseRequest: AllocateLicenseRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).allocateLicense(allocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public confirmUser(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).confirmUser(confirmRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {ConfirmRequest} confirmRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public confirmUserAndInitPassword(confirmRequest: ConfirmRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).confirmUserAndInitPassword(confirmRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ライセンス割り当てを解除します + * @summary + * @param {DeallocateLicenseRequest} deallocateLicenseRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deallocateLicense(deallocateLicenseRequest: DeallocateLicenseRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deallocateLicense(deallocateLicenseRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーを削除します + * @summary + * @param {PostDeleteUserRequest} postDeleteUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public deleteUser(postDeleteUserRequest: PostDeleteUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).deleteUser(postDeleteUserRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーの情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getMyUser(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getMyUser(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーに関連する各種情報を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getRelations(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getRelations(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのタスクソート条件を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getSortCriteria(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getSortCriteria(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public getUsers(options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).getUsers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーを一括登録します + * @summary + * @param {PostMultipleImportsRequest} postMultipleImportsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImports(postMultipleImportsRequest: PostMultipleImportsRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImports(postMultipleImportsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザー一括登録の完了を通知します + * @summary + * @param {PostMultipleImportsCompleteRequest} postMultipleImportsCompleteRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public multipleImportsComplate(postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).multipleImportsComplate(postMultipleImportsCompleteRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary + * @param {SignupRequest} signupRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public signup(signupRequest: SignupRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).signup(signupRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * 利用規約同意バージョンを更新 + * @summary + * @param {UpdateAcceptedVersionRequest} updateAcceptedVersionRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateAcceptedVersion(updateAcceptedVersionRequest: UpdateAcceptedVersionRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateAcceptedVersion(updateAcceptedVersionRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ログインしているユーザーのタスクソート条件を更新します + * @summary + * @param {PostSortCriteriaRequest} postSortCriteriaRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateSortCriteria(postSortCriteriaRequest: PostSortCriteriaRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateSortCriteria(postSortCriteriaRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * ユーザーの情報を更新します + * @summary + * @param {PostUpdateUserRequest} postUpdateUserRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UsersApi + */ + public updateUser(postUpdateUserRequest: PostUpdateUserRequest, options?: AxiosRequestConfig) { + return UsersApiFp(this.configuration).updateUser(postUpdateUserRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + +/** + * WorkflowsApi - axios parameter creator + * @export + */ +export const WorkflowsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflows: async (createWorkflowsRequest: CreateWorkflowsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'createWorkflowsRequest' is not null or undefined + assertParamExists('createWorkflows', 'createWorkflowsRequest', createWorkflowsRequest) + const localVarPath = `/workflows`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createWorkflowsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorkflow: async (workflowId: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'workflowId' is not null or undefined + assertParamExists('deleteWorkflow', 'workflowId', workflowId) + const localVarPath = `/workflows/{workflowId}/delete` + .replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorkflows: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/workflows`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorkflow: async (workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'workflowId' is not null or undefined + assertParamExists('updateWorkflow', 'workflowId', workflowId) + // verify required parameter 'updateWorkflowRequest' is not null or undefined + assertParamExists('updateWorkflow', 'updateWorkflowRequest', updateWorkflowRequest) + const localVarPath = `/workflows/{workflowId}` + .replace(`{${"workflowId"}}`, encodeURIComponent(String(workflowId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updateWorkflowRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * WorkflowsApi - functional programming interface + * @export + */ +export const WorkflowsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = WorkflowsApiAxiosParamCreator(configuration) + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createWorkflows(createWorkflowsRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.createWorkflows']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteWorkflow(workflowId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteWorkflow(workflowId, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.deleteWorkflow']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getWorkflows(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getWorkflows(options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.getWorkflows']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateWorkflow(workflowId, updateWorkflowRequest, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['WorkflowsApi.updateWorkflow']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, + } +}; + +/** + * WorkflowsApi - factory interface + * @export + */ +export const WorkflowsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = WorkflowsApiFp(configuration) + return { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: any): AxiosPromise { + return localVarFp.createWorkflows(createWorkflowsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteWorkflow(workflowId: number, options?: any): AxiosPromise { + return localVarFp.deleteWorkflow(workflowId, options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getWorkflows(options?: any): AxiosPromise { + return localVarFp.getWorkflows(options).then((request) => request(axios, basePath)); + }, + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: any): AxiosPromise { + return localVarFp.updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * WorkflowsApi - object-oriented interface + * @export + * @class WorkflowsApi + * @extends {BaseAPI} + */ +export class WorkflowsApi extends BaseAPI { + /** + * アカウント内にワークフローを新規作成します + * @summary + * @param {CreateWorkflowsRequest} createWorkflowsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public createWorkflows(createWorkflowsRequest: CreateWorkflowsRequest, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).createWorkflows(createWorkflowsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローを削除します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public deleteWorkflow(workflowId: number, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).deleteWorkflow(workflowId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローの一覧を取得します + * @summary + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public getWorkflows(options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).getWorkflows(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * アカウント内のワークフローを編集します + * @summary + * @param {number} workflowId ワークフローの内部ID + * @param {UpdateWorkflowRequest} updateWorkflowRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof WorkflowsApi + */ + public updateWorkflow(workflowId: number, updateWorkflowRequest: UpdateWorkflowRequest, options?: AxiosRequestConfig) { + return WorkflowsApiFp(this.configuration).updateWorkflow(workflowId, updateWorkflowRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + + diff --git a/dictation_function/src/api/base.ts b/dictation_function/src/api/base.ts new file mode 100644 index 0000000..4e44af5 --- /dev/null +++ b/dictation_function/src/api/base.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath ?? basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} + +interface ServerMap { + [key: string]: { + url: string, + description: string, + }[]; +} + +/** + * + * @export + */ +export const operationServerMap: ServerMap = { +} diff --git a/dictation_function/src/api/common.ts b/dictation_function/src/api/common.ts new file mode 100644 index 0000000..0e1c39c --- /dev/null +++ b/dictation_function/src/api/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/dictation_function/src/api/configuration.ts b/dictation_function/src/api/configuration.ts new file mode 100644 index 0000000..9941122 --- /dev/null +++ b/dictation_function/src/api/configuration.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + serverIndex?: number; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * override server index + * + * @type {number} + * @memberof Configuration + */ + serverIndex?: number; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.serverIndex = param.serverIndex; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/dictation_function/src/api/git_push.sh b/dictation_function/src/api/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/dictation_function/src/api/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/dictation_function/src/api/index.ts b/dictation_function/src/api/index.ts new file mode 100644 index 0000000..c982723 --- /dev/null +++ b/dictation_function/src/api/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * ODMSOpenAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/dictation_function/src/api/odms/openapi.json b/dictation_function/src/api/odms/openapi.json new file mode 100644 index 0000000..c279e1f --- /dev/null +++ b/dictation_function/src/api/odms/openapi.json @@ -0,0 +1,5357 @@ +{ + "openapi": "3.0.0", + "paths": { + "/health": { + "get": { + "operationId": "checkHealth", + "summary": "", + "parameters": [], + "responses": { "200": { "description": "" } } + } + }, + "/auth/token": { + "post": { + "operationId": "token", + "summary": "", + "description": "AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TokenRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TokenResponse" } + } + } + }, + "401": { + "description": "認証エラー/同意済み利用規約が最新でない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"] + } + }, + "/auth/accessToken": { + "post": { + "operationId": "accessToken", + "summary": "", + "description": "リフレッシュトークンを元にアクセストークンを再生成します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AccessTokenResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/auth/delegation/token": { + "post": { + "operationId": "delegationToken", + "summary": "", + "description": "代行操作用のリフレッシュトークン・アクセストークンを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationTokenRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationTokenResponse" + } + } + } + }, + "400": { + "description": "指定したアカウントが代行操作を許可していない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/auth/delegation/access-token": { + "post": { + "operationId": "delegationAccessToken", + "summary": "", + "description": "代行操作用のアクセストークンを再生成します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DelegationAccessTokenResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["auth"], + "security": [{ "bearer": [] }] + } + }, + "/accounts": { + "post": { + "operationId": "createAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateAccountRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAccountResponse" + } + } + } + }, + "400": { + "description": "登録済みユーザーからの登録など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/licenses/summary": { + "post": { + "operationId": "getLicenseSummary", + "summary": "", + "description": "指定したアカウントのライセンス集計情報を取得します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLicenseSummaryRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetLicenseSummaryResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/me": { + "get": { + "operationId": "getMyAccount", + "summary": "", + "description": "ログインしているユーザーのアカウント情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMyAccountResponse" + } + } + } + }, + "400": { + "description": "該当アカウントがDBに存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateAccountInfo", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在/管理者ユーザ不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/authors": { + "get": { + "operationId": "getAuthors", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のAuthor一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetAuthorsResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typists": { + "get": { + "operationId": "getTypists", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピスト一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetTypistsResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups": { + "get": { + "operationId": "getTypistGroups", + "summary": "", + "description": "ログインしているユーザーのアカウント配下のタイピストグループ一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistGroupsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下にタイピストグループを追加します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループ名が空の場合/ユーザーが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups/{typistGroupId}": { + "get": { + "operationId": "getTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを取得します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを更新します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTypistGroupRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTypistGroupResponse" + } + } + } + }, + "400": { + "description": "グループ名が空の場合/ユーザーが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/typist-groups/{typistGroupId}/delete": { + "post": { + "operationId": "deleteTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTypistGroupResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner": { + "post": { + "operationId": "createPartnerAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePartnerAccountRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePartnerAccountResponse" + } + } + } + }, + "400": { + "description": "登録済みユーザーからの登録など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner-licenses": { + "post": { + "operationId": "getPartnerLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/order-histories": { + "post": { + "operationId": "getOrderHistories", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrderHistoriesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrderHistoriesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/licenses/issue": { + "post": { + "operationId": "issueLicense", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/IssueLicenseRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueLicenseResponse" + } + } + } + }, + "400": { + "description": "自身のライセンス数が不足している場合/すでに対象注文が発行済の場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/dealers": { + "get": { + "operationId": "getDealers", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetDealersResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/issue/cancel": { + "post": { + "operationId": "cancelIssue", + "summary": "", + "description": "ライセンス発行をキャンセルします", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelIssueRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelIssueResponse" } + } + } + }, + "400": { + "description": "対象注文のステータスが発行済以外/発行日から15日以降/ライセンスをユーザに割り当てている", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes": { + "get": { + "operationId": "getWorktypes", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWorktypesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createWorktype", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorktypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが重複 / WorktypeIDが空 / WorktypeIDが20件登録済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}": { + "post": { + "operationId": "updateWorktype", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorktypesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが重複 / WorktypeIDが空", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}/delete": { + "post": { + "operationId": "deleteWorktype", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteWorktypeResponse" + } + } + } + }, + "400": { + "description": "指定WorktypeIDが削除済み / 指定WorktypeIDがWorkflowで使用中", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/worktypes/{id}/option-items": { + "get": { + "operationId": "getOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "updateOptionItems", + "summary": "", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Worktypeの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOptionItemsResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/active-worktype": { + "post": { + "operationId": "activeWorktype", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostActiveWorktypeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostActiveWorktypeResponse" + } + } + } + }, + "400": { + "description": "WorktypeIDが存在しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partners": { + "get": { + "operationId": "getPartners", + "summary": "", + "parameters": [ + { + "name": "limit", + "required": true, + "in": "query", + "description": "取得件数", + "schema": { "type": "number" } + }, + { + "name": "offset", + "required": true, + "in": "query", + "description": "開始位置", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetPartnersResponse" } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/me/file-delete-setting": { + "post": { + "operationId": "updateFileDeleteSetting", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/delete": { + "post": { + "operationId": "deleteAccountAndData", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/DeleteAccountRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAccountInfoResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "DBアクセスに失敗しログインできる状態で処理が終了した場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/minimal-access": { + "post": { + "operationId": "getAccountInfoMinimalAccess", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAccountInfoMinimalAccessRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAccountInfoMinimalAccessResponse" + } + } + } + }, + "400": { + "description": "対象のユーザーIDが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"] + } + }, + "/accounts/company-name": { + "post": { + "operationId": "getCompanyName", + "summary": "", + "description": "指定したアカウントの会社名を取得します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetCompanyNameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCompanyNameResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/restriction-status": { + "post": { + "operationId": "updateRestrictionStatus", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/users/confirm": { + "post": { + "operationId": "confirmUser", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmResponse" } + } + } + }, + "400": { + "description": "不正なトークン", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users/confirm/initpassword": { + "post": { + "operationId": "confirmUserAndInitPassword", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConfirmResponse" } + } + } + }, + "400": { + "description": "不正なトークン", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users": { + "get": { + "operationId": "getUsers", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetUsersResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/signup": { + "post": { + "operationId": "signup", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SignupRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SignupResponse" } + } + } + }, + "400": { + "description": "登録済みメールによる再登録、AuthorIDの重複など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/relations": { + "get": { + "operationId": "getRelations", + "summary": "", + "description": "ログインしているユーザーに関連する各種情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetRelationsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/sort-criteria": { + "post": { + "operationId": "updateSortCriteria", + "summary": "", + "description": "ログインしているユーザーのタスクソート条件を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostSortCriteriaRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostSortCriteriaResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + }, + "get": { + "operationId": "getSortCriteria", + "summary": "", + "description": "ログインしているユーザーのタスクソート条件を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSortCriteriaResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/update": { + "post": { + "operationId": "updateUser", + "summary": "", + "description": "ユーザーの情報を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostUpdateUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostUpdateUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/license/allocate": { + "post": { + "operationId": "allocateLicense", + "summary": "", + "description": "ライセンスを割り当てます", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllocateLicenseRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllocateLicenseResponse" + } + } + } + }, + "400": { + "description": "割り当て失敗時", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/license/deallocate": { + "post": { + "operationId": "deallocateLicense", + "summary": "", + "description": "ライセンス割り当てを解除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeallocateLicenseRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeallocateLicenseResponse" + } + } + } + }, + "400": { + "description": "すでにライセンスが割り当て解除されている時", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/accepted-version": { + "post": { + "operationId": "updateAcceptedVersion", + "summary": "", + "description": "利用規約同意バージョンを更新", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAcceptedVersionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAcceptedVersionResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/対象のユーザidが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"] + } + }, + "/users/me": { + "get": { + "operationId": "getMyUser", + "summary": "", + "description": "ログインしているユーザーの情報を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/GetMyUserResponse" } + } + } + }, + "400": { + "description": "該当ユーザーがDBに存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/delete": { + "post": { + "operationId": "deleteUser", + "summary": "", + "description": "ユーザーを削除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostDeleteUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports": { + "post": { + "operationId": "multipleImports", + "summary": "", + "description": "ユーザーを一括登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports/complete": { + "post": { + "operationId": "multipleImportsComplate", + "summary": "", + "description": "ユーザー一括登録の完了を通知します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/upload-finished": { + "post": { + "operationId": "uploadFinished", + "summary": "", + "description": "アップロードが完了した音声ファイルの情報を登録し、文字起こしタスクを生成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadFinishedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadFinishedResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/upload-location": { + "get": { + "operationId": "uploadLocation", + "summary": "", + "description": "ログイン中ユーザー用のBlob Storage上の音声ファイルのアップロード先アクセスURLを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioUploadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/audio/download-location": { + "get": { + "operationId": "downloadLocation", + "summary": "", + "description": "指定した音声ファイルのBlob Storage上のダウンロード先アクセスURLを取得します", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "query", + "description": "ODMSCloud上で管理する音声ファイルのID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioDownloadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/download-location": { + "get": { + "operationId": "downloadTemplateLocation", + "summary": "", + "description": "指定した音声ファイルに対応したテンプレートファイルのBlob Storage上のダウンロード先アクセスURLを取得します", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "query", + "description": "文字起こし対象の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateDownloadLocationResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/upload-location": { + "get": { + "operationId": "uploadTemplateLocation", + "summary": "", + "description": "ログイン中ユーザー用のBlob Storage上のテンプレートファイルのアップロード先アクセスURLを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadLocationResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/files/template/upload-finished": { + "post": { + "operationId": "uploadTemplateFinished", + "summary": "", + "description": "アップロードが完了したテンプレートファイルの情報を登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TemplateUploadFinishedReqponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, + "/tasks": { + "get": { + "operationId": "getTasks", + "summary": "", + "description": "音声ファイル・文字起こしタスク情報をページ指定して取得します", + "parameters": [ + { + "name": "limit", + "required": false, + "in": "query", + "description": "タスクの取得件数(指定しない場合はデフォルト値)", + "schema": { "default": 200, "type": "number" } + }, + { + "name": "offset", + "required": false, + "in": "query", + "description": "オフセット(何件目から取得するか 設定しない場合はデフォルト値)", + "schema": { "default": 0, "type": "number" } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "取得対象とするタスクのステータス。カンマ(,)区切りで複数指定可能。設定されない場合はすべてのステータスを取得対象とする。許容するステータスの値は次の通り: Uploaded / Pending / InProgress / Finished / Backup", + "example": "Uploaded,Pending,InProgress", + "schema": { "type": "string" } + }, + { + "name": "direction", + "required": false, + "in": "query", + "description": "ASC/DESC", + "schema": { "type": "string" } + }, + { + "name": "paramName", + "required": false, + "in": "query", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TasksResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/next": { + "get": { + "operationId": "getNextAudioFile", + "summary": "", + "description": "指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します", + "parameters": [ + { + "name": "endedFileId", + "required": true, + "in": "query", + "description": "文字起こし完了したタスクの音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AudioNextResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkout": { + "post": { + "operationId": "checkout", + "summary": "", + "description": "指定した文字起こしタスクをチェックアウトします(ステータスをInprogressにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkin": { + "post": { + "operationId": "checkin", + "summary": "", + "description": "指定した文字起こしタスクをチェックインします(ステータスをFinishedにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/cancel": { + "post": { + "operationId": "cancel", + "summary": "", + "description": "指定した文字起こしタスクをキャンセルします(ステータスをUploadedにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/suspend": { + "post": { + "operationId": "suspend", + "summary": "", + "description": "指定した文字起こしタスクを一時中断します(ステータスをPendingにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/backup": { + "post": { + "operationId": "backup", + "summary": "", + "description": "指定した文字起こしタスクをバックアップします(ステータスをBackupにします)", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/checkout-permission": { + "post": { + "operationId": "changeCheckoutPermission", + "summary": "", + "description": "指定した文字起こしタスクのチェックアウト候補を変更します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostCheckoutPermissionResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ(タスクのステータス不正、指定ユーザー不正など)", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "404": { + "description": "指定したIDの音声ファイルが存在しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/tasks/{audioFileId}/delete": { + "post": { + "operationId": "deleteTask", + "summary": "", + "description": "指定した文字起こしタスクを削除します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteTaskResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/orders": { + "post": { + "operationId": "createOrders", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CreateOrdersRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrdersResponse" + } + } + } + }, + "400": { + "description": "同一PONumberの注文がすでに存在する場合など", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/cards": { + "post": { + "operationId": "issueCardLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueCardLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueCardLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/cards/activate": { + "post": { + "operationId": "activateCardLicenses", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateCardLicensesRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivateCardLicensesResponse" + } + } + } + }, + "400": { + "description": "パラメータのライセンスキーが不正な内容の場合/存在しない場合/登録済みの場合", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/allocatable": { + "get": { + "operationId": "getAllocatableLicenses", + "summary": "", + "description": "割り当て可能なライセンスを取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllocatableLicensesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/licenses/orders/cancel": { + "post": { + "operationId": "cancelOrder", + "summary": "", + "description": "ライセンス注文をキャンセルします", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelOrderRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CancelOrderResponse" } + } + } + }, + "400": { + "description": "対象注文のステータスが発行待ち状態でないとき", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["licenses"], + "security": [{ "bearer": [] }] + } + }, + "/templates": { + "get": { + "operationId": "getTemplates", + "summary": "", + "description": "アカウント内のテンプレートファイルの一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTemplatesResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, + "/templates/{templateFileId}/delete": { + "post": { + "operationId": "deleteTemplateFile", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します", + "parameters": [ + { + "name": "templateFileId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTemplateResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, + "/workflows": { + "get": { + "operationId": "getWorkflows", + "summary": "", + "description": "アカウント内のワークフローの一覧を取得します", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetWorkflowsResponse" + } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + }, + "post": { + "operationId": "createWorkflows", + "summary": "", + "description": "アカウント内にワークフローを新規作成します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkflowsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkflowsResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/workflows/{workflowId}": { + "post": { + "operationId": "updateWorkflow", + "summary": "", + "description": "アカウント内のワークフローを編集します", + "parameters": [ + { + "name": "workflowId", + "required": true, + "in": "path", + "description": "ワークフローの内部ID", + "schema": { "type": "number" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateWorkflowRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkflowResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/workflows/{workflowId}/delete": { + "post": { + "operationId": "deleteWorkflow", + "summary": "", + "description": "アカウント内のワークフローを削除します", + "parameters": [ + { + "name": "workflowId", + "required": true, + "in": "path", + "description": "ワークフローの内部ID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteWorkflowResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["workflows"], + "security": [{ "bearer": [] }] + } + }, + "/notification/register": { + "post": { + "operationId": "register", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegisterRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RegisterResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["notification"], + "security": [{ "bearer": [] }] + } + }, + "/terms": { + "get": { + "operationId": "getTermsInfo", + "summary": "", + "parameters": [], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTermsInfoResponse" + } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["terms"] + } + } + }, + "info": { + "title": "ODMSOpenAPI", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "securitySchemes": { + "bearer": { "scheme": "bearer", "bearerFormat": "JWT", "type": "http" } + }, + "schemas": { + "TokenRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string" }, + "type": { + "type": "string", + "description": "web or mobile or desktop" + } + }, + "required": ["idToken", "type"] + }, + "TokenResponse": { + "type": "object", + "properties": { + "refreshToken": { "type": "string" }, + "accessToken": { "type": "string" } + }, + "required": ["refreshToken", "accessToken"] + }, + "ErrorResponse": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "code": { "type": "string" } + }, + "required": ["message", "code"] + }, + "AccessTokenResponse": { + "type": "object", + "properties": { "accessToken": { "type": "string" } }, + "required": ["accessToken"] + }, + "DelegationTokenRequest": { + "type": "object", + "properties": { + "delegatedAccountId": { + "type": "number", + "description": "代行操作対象のアカウントID" + } + }, + "required": ["delegatedAccountId"] + }, + "DelegationTokenResponse": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string", + "description": "代行操作用のリフレッシュトークン" + }, + "accessToken": { + "type": "string", + "description": "代行操作用のアクセストークン" + } + }, + "required": ["refreshToken", "accessToken"] + }, + "DelegationAccessTokenResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "description": "代行操作用のアクセストークン" + } + }, + "required": ["accessToken"] + }, + "CreateAccountRequest": { + "type": "object", + "properties": { + "companyName": { "type": "string" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)", + "minLength": 2, + "maxLength": 2 + }, + "dealerAccountId": { "type": "number" }, + "adminName": { "type": "string" }, + "adminMail": { "type": "string" }, + "adminPassword": { "type": "string" }, + "acceptedEulaVersion": { + "type": "string", + "description": "同意済み利用規約のバージョン(EULA)" + }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "同意済みプライバシーポリシーのバージョン" + }, + "acceptedDpaVersion": { + "type": "string", + "description": "同意済み利用規約のバージョン(DPA)" + }, + "token": { "type": "string", "description": "reCAPTCHA Token" } + }, + "required": [ + "companyName", + "country", + "adminName", + "adminMail", + "adminPassword", + "acceptedEulaVersion", + "acceptedPrivacyNoticeVersion", + "acceptedDpaVersion", + "token" + ] + }, + "CreateAccountResponse": { "type": "object", "properties": {} }, + "GetLicenseSummaryRequest": { + "type": "object", + "properties": { "accountId": { "type": "number" } }, + "required": ["accountId"] + }, + "GetLicenseSummaryResponse": { + "type": "object", + "properties": { + "totalLicense": { "type": "number" }, + "allocatedLicense": { "type": "number" }, + "reusableLicense": { "type": "number" }, + "freeLicense": { "type": "number" }, + "expiringWithin14daysLicense": { "type": "number" }, + "issueRequesting": { "type": "number" }, + "numberOfRequesting": { "type": "number" }, + "shortage": { "type": "number" }, + "storageSize": { "type": "number" }, + "usedSize": { "type": "number" }, + "isStorageAvailable": { "type": "boolean" } + }, + "required": [ + "totalLicense", + "allocatedLicense", + "reusableLicense", + "freeLicense", + "expiringWithin14daysLicense", + "issueRequesting", + "numberOfRequesting", + "shortage", + "storageSize", + "usedSize", + "isStorageAvailable" + ] + }, + "Account": { + "type": "object", + "properties": { + "accountId": { "type": "number" }, + "companyName": { "type": "string" }, + "tier": { "type": "number" }, + "country": { "type": "string" }, + "parentAccountId": { "type": "number" }, + "delegationPermission": { "type": "boolean" }, + "autoFileDelete": { "type": "boolean" }, + "fileRetentionDays": { "type": "number" }, + "primaryAdminUserId": { "type": "number" }, + "secondryAdminUserId": { "type": "number" }, + "parentAccountName": { "type": "string" } + }, + "required": [ + "accountId", + "companyName", + "tier", + "country", + "delegationPermission", + "autoFileDelete", + "fileRetentionDays" + ] + }, + "GetMyAccountResponse": { + "type": "object", + "properties": { "account": { "$ref": "#/components/schemas/Account" } }, + "required": ["account"] + }, + "Author": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "Authorユーザーの内部ID" }, + "authorId": { "type": "string", "description": "AuthorID" } + }, + "required": ["id", "authorId"] + }, + "GetAuthorsResponse": { + "type": "object", + "properties": { + "authors": { + "type": "array", + "items": { "$ref": "#/components/schemas/Author" } + } + }, + "required": ["authors"] + }, + "Typist": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "TypistのユーザーID" }, + "name": { "type": "string", "description": "Typistのユーザー名" } + }, + "required": ["id", "name"] + }, + "GetTypistsResponse": { + "type": "object", + "properties": { + "typists": { + "type": "array", + "items": { "$ref": "#/components/schemas/Typist" } + } + }, + "required": ["typists"] + }, + "TypistGroup": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "TypistGroupのID" }, + "name": { "type": "string", "description": "TypistGroup名" } + }, + "required": ["id", "name"] + }, + "GetTypistGroupsResponse": { + "type": "object", + "properties": { + "typistGroups": { + "type": "array", + "items": { "$ref": "#/components/schemas/TypistGroup" } + } + }, + "required": ["typistGroups"] + }, + "GetTypistGroupResponse": { + "type": "object", + "properties": { + "typistGroupName": { "type": "string" }, + "typistIds": { "type": "array", "items": { "type": "integer" } } + }, + "required": ["typistGroupName", "typistIds"] + }, + "CreateTypistGroupRequest": { + "type": "object", + "properties": { + "typistGroupName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "typistIds": { + "minItems": 1, + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["typistGroupName", "typistIds"] + }, + "CreateTypistGroupResponse": { "type": "object", "properties": {} }, + "UpdateTypistGroupRequest": { + "type": "object", + "properties": { + "typistGroupName": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "typistIds": { + "minItems": 1, + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["typistGroupName", "typistIds"] + }, + "DeleteTypistGroupResponse": { "type": "object", "properties": {} }, + "CreatePartnerAccountRequest": { + "type": "object", + "properties": { + "companyName": { "type": "string" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)", + "minLength": 2, + "maxLength": 2 + }, + "adminName": { "type": "string" }, + "email": { "type": "string" } + }, + "required": ["companyName", "country", "adminName", "email"] + }, + "CreatePartnerAccountResponse": { "type": "object", "properties": {} }, + "GetPartnerLicensesRequest": { + "type": "object", + "properties": { + "limit": { "type": "number" }, + "offset": { "type": "number" }, + "accountId": { "type": "number" } + }, + "required": ["limit", "offset", "accountId"] + }, + "PartnerLicenseInfo": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "tier": { "type": "number", "description": "階層" }, + "companyName": { "type": "string", "description": "アカウント名" }, + "stockLicense": { + "type": "number", + "description": "保有している有効期限が未設定あるいは有効期限内のライセンス数" + }, + "issuedRequested": { + "type": "number", + "description": "子アカウントからの、未発行状態あるいは発行キャンセルされた注文の総ライセンス数" + }, + "shortage": { + "type": "number", + "description": "不足数({Stock license} - {Issue Requested})" + }, + "issueRequesting": { + "type": "number", + "description": "未発行状態あるいは発行キャンセルされた注文の総ライセンス数(=IssueRequestingのStatusの注文の総ライセンス数)" + } + }, + "required": [ + "accountId", + "tier", + "companyName", + "stockLicense", + "issuedRequested", + "shortage", + "issueRequesting" + ] + }, + "GetPartnerLicensesResponse": { + "type": "object", + "properties": { + "total": { "type": "number" }, + "ownPartnerLicense": { + "$ref": "#/components/schemas/PartnerLicenseInfo" + }, + "childrenPartnerLicenses": { + "type": "array", + "items": { "$ref": "#/components/schemas/PartnerLicenseInfo" } + } + }, + "required": ["total", "ownPartnerLicense", "childrenPartnerLicenses"] + }, + "GetOrderHistoriesRequest": { + "type": "object", + "properties": { + "limit": { "type": "number", "description": "取得件数" }, + "offset": { "type": "number", "description": "開始位置" }, + "accountId": { "type": "number", "description": "アカウントID" } + }, + "required": ["limit", "offset", "accountId"] + }, + "LicenseOrder": { + "type": "object", + "properties": { + "orderDate": { "type": "string", "description": "注文日付" }, + "issueDate": { "type": "string", "description": "発行日付" }, + "numberOfOrder": { "type": "number", "description": "注文数" }, + "poNumber": { "type": "string", "description": "POナンバー" }, + "status": { "type": "string", "description": "注文状態" } + }, + "required": ["orderDate", "numberOfOrder", "poNumber", "status"] + }, + "GetOrderHistoriesResponse": { + "type": "object", + "properties": { + "total": { "type": "number", "description": "合計件数" }, + "orderHistories": { + "type": "array", + "items": { "$ref": "#/components/schemas/LicenseOrder" } + } + }, + "required": ["total", "orderHistories"] + }, + "IssueLicenseRequest": { + "type": "object", + "properties": { + "orderedAccountId": { + "type": "number", + "description": "注文元アカウントID" + }, + "poNumber": { "type": "string", "description": "POナンバー" } + }, + "required": ["orderedAccountId", "poNumber"] + }, + "IssueLicenseResponse": { "type": "object", "properties": {} }, + "Dealer": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "アカウントID" }, + "name": { "type": "string", "description": "会社名" }, + "country": { + "type": "string", + "description": "国名(ISO 3166-1 alpha-2)" + } + }, + "required": ["id", "name", "country"] + }, + "GetDealersResponse": { + "type": "object", + "properties": { + "dealers": { + "type": "array", + "items": { "$ref": "#/components/schemas/Dealer" } + } + }, + "required": ["dealers"] + }, + "CancelIssueRequest": { + "type": "object", + "properties": { + "orderedAccountId": { + "type": "number", + "description": "注文元アカウントID" + }, + "poNumber": { "type": "string", "description": "POナンバー" } + }, + "required": ["orderedAccountId", "poNumber"] + }, + "CancelIssueResponse": { "type": "object", "properties": {} }, + "Worktype": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "WorktypeのID" }, + "worktypeId": { "type": "string", "description": "WorktypeID" }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["id", "worktypeId"] + }, + "GetWorktypesResponse": { + "type": "object", + "properties": { + "worktypes": { + "type": "array", + "items": { "$ref": "#/components/schemas/Worktype" } + }, + "active": { + "type": "number", + "description": "Active WorktypeIDに設定されているWorkTypeの内部ID" + } + }, + "required": ["worktypes"] + }, + "CreateWorktypesRequest": { + "type": "object", + "properties": { + "worktypeId": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "WorktypeID" + }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["worktypeId"] + }, + "CreateWorktypeResponse": { "type": "object", "properties": {} }, + "UpdateWorktypesRequest": { + "type": "object", + "properties": { + "worktypeId": { + "type": "string", + "minLength": 1, + "description": "WorktypeID" + }, + "description": { "type": "string", "description": "Worktypeの説明" } + }, + "required": ["worktypeId"] + }, + "UpdateWorktypeResponse": { "type": "object", "properties": {} }, + "DeleteWorktypeResponse": { "type": "object", "properties": {} }, + "GetWorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 }, + "id": { "type": "number" } + }, + "required": ["itemLabel", "defaultValueType", "initialValue", "id"] + }, + "GetOptionItemsResponse": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/GetWorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "PostWorktypeOptionItem": { + "type": "object", + "properties": { + "itemLabel": { "type": "string", "maxLength": 16 }, + "defaultValueType": { + "type": "string", + "maxLength": 20, + "description": "Default / Blank / LastInput" + }, + "initialValue": { "type": "string", "maxLength": 20 } + }, + "required": ["itemLabel", "defaultValueType", "initialValue"] + }, + "UpdateOptionItemsRequest": { + "type": "object", + "properties": { + "optionItems": { + "maxItems": 10, + "minItems": 10, + "type": "array", + "items": { "$ref": "#/components/schemas/PostWorktypeOptionItem" } + } + }, + "required": ["optionItems"] + }, + "UpdateOptionItemsResponse": { "type": "object", "properties": {} }, + "PostActiveWorktypeRequest": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Active WorkTypeIDにするWorktypeの内部ID" + } + } + }, + "PostActiveWorktypeResponse": { "type": "object", "properties": {} }, + "Partner": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "会社名" }, + "tier": { "type": "number", "description": "階層" }, + "accountId": { "type": "number", "description": "アカウントID" }, + "country": { "type": "string", "description": "国" }, + "primaryAdmin": { + "type": "string", + "description": "プライマリ管理者" + }, + "email": { + "type": "string", + "description": "プライマリ管理者メールアドレス" + }, + "dealerManagement": { + "type": "boolean", + "description": "代行操作許可" + } + }, + "required": [ + "name", + "tier", + "accountId", + "country", + "primaryAdmin", + "email", + "dealerManagement" + ] + }, + "GetPartnersResponse": { + "type": "object", + "properties": { + "total": { "type": "number", "description": "合計件数" }, + "partners": { + "type": "array", + "items": { "$ref": "#/components/schemas/Partner" } + } + }, + "required": ["total", "partners"] + }, + "UpdateAccountInfoRequest": { + "type": "object", + "properties": { + "parentAccountId": { + "type": "number", + "description": "親アカウントのID" + }, + "delegationPermission": { + "type": "boolean", + "description": "代行操作許可" + }, + "primaryAdminUserId": { + "type": "number", + "description": "プライマリ管理者ID" + }, + "secondryAdminUserId": { + "type": "number", + "description": "セカンダリ管理者ID" + } + }, + "required": ["delegationPermission", "primaryAdminUserId"] + }, + "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, + "UpdateFileDeleteSettingRequest": { + "type": "object", + "properties": { + "autoFileDelete": { + "type": "boolean", + "description": "自動ファイル削除をするかどうか" + }, + "retentionDays": { + "type": "number", + "description": "文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間" + } + }, + "required": ["autoFileDelete", "retentionDays"] + }, + "UpdateFileDeleteSettingResponse": { "type": "object", "properties": {} }, + "DeleteAccountRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" } + }, + "required": ["accountId"] + }, + "GetAccountInfoMinimalAccessRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string", "description": "idトークン" } + }, + "required": ["idToken"] + }, + "GetAccountInfoMinimalAccessResponse": { + "type": "object", + "properties": { "tier": { "type": "number", "description": "階層" } }, + "required": ["tier"] + }, + "GetCompanyNameRequest": { + "type": "object", + "properties": { "accountId": { "type": "number" } }, + "required": ["accountId"] + }, + "GetCompanyNameResponse": { + "type": "object", + "properties": { "companyName": { "type": "string" } }, + "required": ["companyName"] + }, + "UpdateRestrictionStatusRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "number", + "description": "操作対象の第五階層アカウントID" + }, + "restricted": { + "type": "boolean", + "description": "制限をかけるかどうか(trur:制限をかける)" + } + }, + "required": ["accountId", "restricted"] + }, + "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, + "ConfirmRequest": { + "type": "object", + "properties": { "token": { "type": "string" } }, + "required": ["token"] + }, + "ConfirmResponse": { "type": "object", "properties": {} }, + "User": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "name": { "type": "string" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "typistGroupName": { "type": "array", "items": { "type": "string" } }, + "email": { "type": "string" }, + "emailVerified": { "type": "boolean" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "prompt": { "type": "boolean" }, + "expiration": { "type": "string" }, + "remaining": { "type": "number" }, + "licenseStatus": { + "type": "string", + "description": "Normal/NoLicense/Alert/Renew" + } + }, + "required": [ + "id", + "name", + "role", + "typistGroupName", + "email", + "emailVerified", + "autoRenew", + "notification", + "encryption", + "prompt", + "licenseStatus" + ] + }, + "GetUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + }, + "required": ["users"] + }, + "SignupRequest": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "email": { "type": "string" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "boolean" } + }, + "required": ["name", "role", "email", "autoRenew", "notification"] + }, + "SignupResponse": { "type": "object", "properties": {} }, + "OptionItem": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "description": "Option Itemのラベル" + }, + "initialValueType": { + "type": "number", + "description": "項目タイプ 1:Blank/2:Default/3:前の値" + }, + "defaultValue": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "description": "typeでDefaultを選択した場合のデフォルト値" + } + }, + "required": ["label", "initialValueType", "defaultValue"] + }, + "OptionItemList": { + "type": "object", + "properties": { + "workTypeId": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "description": "1WorkTypeIDにつき、10個まで登録可能", + "type": "array", + "items": { "$ref": "#/components/schemas/OptionItem" } + } + }, + "required": ["workTypeId", "optionItemList"] + }, + "GetRelationsResponse": { + "type": "object", + "properties": { + "authorId": { + "type": "string", + "description": "ログインしたユーザーのAuthorID(Authorでない場合はundefined)" + }, + "authorIdList": { + "description": "属しているアカウントのAuthorID List(全て)", + "type": "array", + "items": { "type": "string" } + }, + "workTypeList": { + "maxItems": 20, + "description": "アカウントに設定されているWorktypeIDのリスト(最大20個)", + "type": "array", + "items": { "$ref": "#/components/schemas/OptionItemList" } + }, + "isEncrypted": { + "type": "boolean", + "description": "ユーザーが音声ファイルを暗号化するかどうか" + }, + "encryptionPassword": { + "type": "string", + "description": "ユーザーが暗号化を掛ける場合のパスワード" + }, + "activeWorktype": { + "type": "string", + "description": "アカウントがデフォルトで利用するWorkTypeID(アカウントに紐づくWorkTypeIDから一つ指定。activeWorktypeがなければ空文字を返却する)" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP): DS2固定" + }, + "prompt": { + "type": "boolean", + "description": "デバイス上で自動的にWorkTypeの選択画面を表示するかどうかのユーザーごとの設定(Authorでない場合はfalse)" + } + }, + "required": [ + "authorIdList", + "workTypeList", + "isEncrypted", + "activeWorktype", + "audioFormat", + "prompt" + ] + }, + "PostSortCriteriaRequest": { + "type": "object", + "properties": { + "direction": { "type": "string", "description": "ASC/DESC" }, + "paramName": { + "type": "string", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE" + } + }, + "required": ["direction", "paramName"] + }, + "PostSortCriteriaResponse": { "type": "object", "properties": {} }, + "GetSortCriteriaResponse": { + "type": "object", + "properties": { + "direction": { "type": "string", "description": "ASC/DESC" }, + "paramName": { + "type": "string", + "description": "JOB_NUMBER/STATUS/ENCRYPTION/AUTHOR_ID/WORK_TYPE/FILE_NAME/FILE_LENGTH/FILE_SIZE/RECORDING_STARTED_DATE/RECORDING_FINISHED_DATE/UPLOAD_DATE/TRANSCRIPTION_STARTED_DATE/TRANSCRIPTION_FINISHED_DATE" + } + }, + "required": ["direction", "paramName"] + }, + "PostUpdateUserRequest": { + "type": "object", + "properties": { + "id": { "type": "number" }, + "role": { "type": "string", "description": "none/author/typist" }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "boolean" }, + "notification": { "type": "boolean" }, + "encryption": { "type": "boolean" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "boolean" } + }, + "required": ["id", "role", "autoRenew", "notification"] + }, + "PostUpdateUserResponse": { "type": "object", "properties": {} }, + "AllocateLicenseRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "ユーザーID" }, + "newLicenseId": { + "type": "number", + "description": "割り当てるライセンスのID" + } + }, + "required": ["userId", "newLicenseId"] + }, + "AllocateLicenseResponse": { "type": "object", "properties": {} }, + "DeallocateLicenseRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "ユーザーID" } + }, + "required": ["userId"] + }, + "DeallocateLicenseResponse": { "type": "object", "properties": {} }, + "UpdateAcceptedVersionRequest": { + "type": "object", + "properties": { + "idToken": { "type": "string", "description": "IDトークン" }, + "acceptedEULAVersion": { + "type": "string", + "description": "更新バージョン(EULA)" + }, + "acceptedPrivacyNoticeVersion": { + "type": "string", + "description": "更新バージョン(PrivacyNotice)" + }, + "acceptedDPAVersion": { + "type": "string", + "description": "更新バージョン(DPA)" + } + }, + "required": [ + "idToken", + "acceptedEULAVersion", + "acceptedPrivacyNoticeVersion" + ] + }, + "UpdateAcceptedVersionResponse": { "type": "object", "properties": {} }, + "GetMyUserResponse": { + "type": "object", + "properties": { + "userName": { "type": "string", "description": "ユーザー名" } + }, + "required": ["userName"] + }, + "PostDeleteUserRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "削除対象のユーザーID" } + }, + "required": ["userId"] + }, + "PostDeleteUserResponse": { "type": "object", "properties": {} }, + "MultipleImportUser": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "role": { + "type": "number", + "description": "0(none)/1(author)/2(typist)" + }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "number", "description": "0(false)/1(true)" }, + "notification": { + "type": "number", + "description": "0(false)/1(true)" + }, + "encryption": { "type": "number", "description": "0(false)/1(true)" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "number", "description": "0(false)/1(true)" } + }, + "required": ["name", "email", "role", "autoRenew", "notification"] + }, + "PostMultipleImportsRequest": { + "type": "object", + "properties": { + "filename": { "type": "string", "description": "CSVファイル名" }, + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportUser" } + } + }, + "required": ["filename", "users"] + }, + "PostMultipleImportsResponse": { "type": "object", "properties": {} }, + "MultipleImportErrors": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "line": { "type": "number", "description": "エラー発生行数" }, + "errorCode": { "type": "string", "description": "エラーコード" } + }, + "required": ["name", "line", "errorCode"] + }, + "PostMultipleImportsCompleteRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "filename": { "type": "string", "description": "CSVファイル名" }, + "requestTime": { + "type": "number", + "description": "一括登録受付時刻(UNIXTIME/ミリ秒)" + }, + "errors": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportErrors" } + } + }, + "required": ["accountId", "filename", "requestTime", "errors"] + }, + "PostMultipleImportsCompleteResponse": { + "type": "object", + "properties": {} + }, + "AudioOptionItem": { + "type": "object", + "properties": { + "optionItemLabel": { + "type": "string", + "minLength": 1, + "maxLength": 16 + }, + "optionItemValue": { + "type": "string", + "minLength": 1, + "maxLength": 20 + } + }, + "required": ["optionItemLabel", "optionItemValue"] + }, + "AudioUploadFinishedRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "アップロード先Blob Storage(ファイル名含む)" + }, + "authorId": { + "type": "string", + "description": "自分自身(ログイン認証)したAuthorID" + }, + "fileName": { "type": "string", "description": "音声ファイル名" }, + "duration": { + "type": "string", + "description": "音声ファイルの録音時間(ミリ秒の整数値)" + }, + "createdDate": { + "type": "string", + "description": "音声ファイルの録音作成日時(開始日時)(yyyy-mm-ddThh:mm:ss.sss)" + }, + "finishedDate": { + "type": "string", + "description": "音声ファイルの録音作成終了日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "uploadedDate": { + "type": "string", + "description": "音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "fileSize": { + "type": "number", + "description": "音声ファイルのファイルサイズ(Byte)" + }, + "priority": { + "type": "string", + "description": "優先度 \"00\":Normal / \"01\":High" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP)" + }, + "comment": { "type": "string" }, + "workType": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "minItems": 10, + "description": "音声ファイルに紐づくOption Itemの一覧(10個固定)", + "type": "array", + "items": { "$ref": "#/components/schemas/AudioOptionItem" } + }, + "isEncrypted": { "type": "boolean" } + }, + "required": [ + "url", + "authorId", + "fileName", + "duration", + "createdDate", + "finishedDate", + "uploadedDate", + "fileSize", + "priority", + "audioFormat", + "comment", + "workType", + "optionItemList", + "isEncrypted" + ] + }, + "AudioUploadFinishedResponse": { + "type": "object", + "properties": { + "jobNumber": { "type": "string", "description": "8桁固定の数字" } + }, + "required": ["jobNumber"] + }, + "AudioUploadLocationResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Blob StorageにアクセスするためのSASトークン入りのアクセスURL" + } + }, + "required": ["url"] + }, + "AudioDownloadLocationResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Blob StorageにアクセスするためのSASトークン入りのアクセスURL" + } + }, + "required": ["url"] + }, + "TemplateDownloadLocationResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] + }, + "TemplateUploadLocationResponse": { + "type": "object", + "properties": { "url": { "type": "string" } }, + "required": ["url"] + }, + "TemplateUploadFinishedRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "テンプレートファイルのファイル名" + }, + "url": { + "type": "string", + "description": "テンプレートファイルのアップロード先URL" + } + }, + "required": ["name", "url"] + }, + "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, + "Assignee": { + "type": "object", + "properties": { + "typistUserId": { + "type": "number", + "description": "TypistID(TypistIDかTypistGroupIDのどちらかに値が入る)" + }, + "typistGroupId": { + "type": "number", + "description": "TypistGroupID(TypistGroupIDかTypistIDのどちらかに値が入る)" + }, + "typistName": { + "type": "string", + "description": "Typist名 / TypistGroup名" + } + }, + "required": ["typistName"] + }, + "Task": { + "type": "object", + "properties": { + "audioFileId": { + "type": "number", + "description": "ODMS Cloud上の音声ファイルID" + }, + "authorId": { "type": "string", "description": "AuthorID" }, + "workType": { "type": "string" }, + "optionItemList": { + "maxItems": 10, + "minItems": 10, + "description": "音声ファイルに紐づくOption Itemの一覧(10個固定)", + "type": "array", + "items": { "$ref": "#/components/schemas/AudioOptionItem" } + }, + "url": { + "type": "string", + "description": "音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL" + }, + "fileName": { "type": "string", "description": "音声ファイル名" }, + "audioDuration": { + "type": "string", + "description": "音声ファイルの録音時間(ミリ秒の整数値)" + }, + "audioCreatedDate": { + "type": "string", + "description": "音声ファイルの録音開始日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "audioFinishedDate": { + "type": "string", + "description": "音声ファイルの録音終了日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "audioUploadedDate": { + "type": "string", + "description": "音声ファイルのアップロード日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "fileSize": { + "type": "number", + "description": "音声ファイルのファイルサイズ(Byte)" + }, + "priority": { + "type": "string", + "description": "音声ファイルの優先度 \"00\":Normal / \"01\":High" + }, + "audioFormat": { + "type": "string", + "description": "録音形式: DSS/DS2(SP)/DS2(QP)" + }, + "comment": { "type": "string", "description": "コメント" }, + "isEncrypted": { "type": "boolean" }, + "jobNumber": { "type": "string", "description": "JOBナンバー" }, + "typist": { + "description": "割り当てられたユーザー", + "allOf": [{ "$ref": "#/components/schemas/Typist" }] + }, + "assignees": { + "description": "文字起こしに着手できる(チェックアウト可能な)、タスクにアサインされているグループ/個人の一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + }, + "status": { + "type": "string", + "description": "音声ファイルのファイルステータス Uploaded / Pending / InProgress / Finished / Backup" + }, + "transcriptionStartedDate": { + "type": "string", + "description": "文字起こし開始日時(yyyy-mm-ddThh:mm:ss.sss)" + }, + "transcriptionFinishedDate": { + "type": "string", + "description": "文字起こし終了日時(yyyy-mm-ddThh:mm:ss.sss)" + } + }, + "required": [ + "audioFileId", + "authorId", + "workType", + "optionItemList", + "url", + "fileName", + "audioDuration", + "audioCreatedDate", + "audioFinishedDate", + "audioUploadedDate", + "fileSize", + "priority", + "audioFormat", + "comment", + "isEncrypted", + "jobNumber", + "assignees", + "status" + ] + }, + "TasksResponse": { + "type": "object", + "properties": { + "limit": { + "type": "number", + "description": "タスクの取得件数(指定しない場合はデフォルト値)" + }, + "offset": { + "type": "number", + "description": "オフセット(何件目から取得するか 設定しない場合はデフォルト値)" + }, + "total": { "type": "number", "description": "タスクの総件数" }, + "tasks": { + "description": "音声ファイル/タスク一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Task" } + } + }, + "required": ["limit", "offset", "total", "tasks"] + }, + "AudioNextResponse": { + "type": "object", + "properties": { + "nextFileId": { + "type": "number", + "description": "ODMS Cloud上の次の音声ファイルID(存在しなければundefind)" + } + } + }, + "ChangeStatusResponse": { "type": "object", "properties": {} }, + "PostCheckoutPermissionRequest": { + "type": "object", + "properties": { + "assignees": { + "description": "文字起こしに着手可能(チェックアウト可能)にしたい、グループ個人の一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + } + }, + "required": ["assignees"] + }, + "PostCheckoutPermissionResponse": { "type": "object", "properties": {} }, + "PostDeleteTaskResponse": { "type": "object", "properties": {} }, + "CreateOrdersRequest": { + "type": "object", + "properties": { + "poNumber": { "type": "string" }, + "orderCount": { "type": "number" } + }, + "required": ["poNumber", "orderCount"] + }, + "CreateOrdersResponse": { "type": "object", "properties": {} }, + "IssueCardLicensesRequest": { + "type": "object", + "properties": { "createCount": { "type": "number" } }, + "required": ["createCount"] + }, + "IssueCardLicensesResponse": { + "type": "object", + "properties": { + "cardLicenseKeys": { "type": "array", "items": { "type": "string" } } + }, + "required": ["cardLicenseKeys"] + }, + "ActivateCardLicensesRequest": { + "type": "object", + "properties": { "cardLicenseKey": { "type": "string" } }, + "required": ["cardLicenseKey"] + }, + "ActivateCardLicensesResponse": { "type": "object", "properties": {} }, + "AllocatableLicenseInfo": { + "type": "object", + "properties": { + "licenseId": { "type": "number" }, + "expiryDate": { "format": "date-time", "type": "string" } + }, + "required": ["licenseId"] + }, + "GetAllocatableLicensesResponse": { + "type": "object", + "properties": { + "allocatableLicenses": { + "type": "array", + "items": { "$ref": "#/components/schemas/AllocatableLicenseInfo" } + } + }, + "required": ["allocatableLicenses"] + }, + "CancelOrderRequest": { + "type": "object", + "properties": { "poNumber": { "type": "string" } }, + "required": ["poNumber"] + }, + "CancelOrderResponse": { "type": "object", "properties": {} }, + "TemplateFile": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "テンプレートファイルのID" }, + "name": { + "type": "string", + "description": "テンプレートファイルのファイル名" + } + }, + "required": ["id", "name"] + }, + "GetTemplatesResponse": { + "type": "object", + "properties": { + "templates": { + "description": "テンプレートファイルの一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/TemplateFile" } + } + }, + "required": ["templates"] + }, + "DeleteTemplateResponse": { "type": "object", "properties": {} }, + "WorkflowWorktype": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "Worktypeの内部ID" }, + "worktypeId": { "type": "string", "description": "WorktypeID" } + }, + "required": ["id", "worktypeId"] + }, + "WorkflowTemplate": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "テンプレートの内部ID" }, + "fileName": { + "type": "string", + "description": "テンプレートのファイル名" + } + }, + "required": ["id", "fileName"] + }, + "Workflow": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "ワークフローの内部ID" }, + "author": { + "description": "Author情報", + "allOf": [{ "$ref": "#/components/schemas/Author" }] + }, + "worktype": { + "description": "Worktype情報", + "allOf": [{ "$ref": "#/components/schemas/WorkflowWorktype" }] + }, + "template": { + "description": "テンプレート情報", + "allOf": [{ "$ref": "#/components/schemas/WorkflowTemplate" }] + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "type": "array", + "items": { "$ref": "#/components/schemas/Assignee" } + } + }, + "required": ["id", "author", "typists"] + }, + "GetWorkflowsResponse": { + "type": "object", + "properties": { + "workflows": { + "description": "ワークフローの一覧", + "type": "array", + "items": { "$ref": "#/components/schemas/Workflow" } + } + }, + "required": ["workflows"] + }, + "WorkflowTypist": { + "type": "object", + "properties": { + "typistId": { + "type": "number", + "description": "タイピストユーザーの内部ID" + }, + "typistGroupId": { + "type": "number", + "description": "タイピストグループの内部ID" + } + } + }, + "CreateWorkflowsRequest": { + "type": "object", + "properties": { + "authorId": { "type": "number", "description": "Authorの内部ID" }, + "worktypeId": { "type": "number", "description": "Worktypeの内部ID" }, + "templateId": { + "type": "number", + "description": "テンプレートの内部ID" + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "minItems": 1, + "type": "array", + "items": { "$ref": "#/components/schemas/WorkflowTypist" } + } + }, + "required": ["authorId", "typists"] + }, + "CreateWorkflowsResponse": { "type": "object", "properties": {} }, + "UpdateWorkflowRequest": { + "type": "object", + "properties": { + "authorId": { "type": "number", "description": "Authorの内部ID" }, + "worktypeId": { "type": "number", "description": "Worktypeの内部ID" }, + "templateId": { + "type": "number", + "description": "テンプレートの内部ID" + }, + "typists": { + "description": "ルーティング候補のタイピストユーザー/タイピストグループ", + "minItems": 1, + "type": "array", + "items": { "$ref": "#/components/schemas/WorkflowTypist" } + } + }, + "required": ["authorId", "typists"] + }, + "UpdateWorkflowResponse": { "type": "object", "properties": {} }, + "DeleteWorkflowResponse": { "type": "object", "properties": {} }, + "RegisterRequest": { + "type": "object", + "properties": { + "pns": { "type": "string", "description": "wns or apns" }, + "handler": { + "type": "string", + "description": "wnsのチャネルURI or apnsのデバイストークン" + } + }, + "required": ["pns", "handler"] + }, + "RegisterResponse": { "type": "object", "properties": {} }, + "TermInfo": { + "type": "object", + "properties": { + "documentType": { "type": "string", "description": "利用規約種別" }, + "version": { "type": "string", "description": "バージョン" } + }, + "required": ["documentType", "version"] + }, + "GetTermsInfoResponse": { + "type": "object", + "properties": { + "termsInfo": { + "type": "array", + "items": { "$ref": "#/components/schemas/TermInfo" } + } + }, + "required": ["termsInfo"] + } + } + } +} diff --git a/dictation_function/src/blobstorage/audioBlobStorage.service.ts b/dictation_function/src/blobstorage/audioBlobStorage.service.ts new file mode 100644 index 0000000..4acf23a --- /dev/null +++ b/dictation_function/src/blobstorage/audioBlobStorage.service.ts @@ -0,0 +1,146 @@ +import { + BlobServiceClient, + ContainerClient, + StorageSharedKeyCredential, +} from "@azure/storage-blob"; +import { + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, +} from "../constants"; +import { InvocationContext } from "@azure/functions"; + +export class AudioBlobStorageService { + private readonly sharedKeyCredentialUS: StorageSharedKeyCredential; + private readonly sharedKeyCredentialEU: StorageSharedKeyCredential; + private readonly sharedKeyCredentialAU: StorageSharedKeyCredential; + + private readonly blobServiceClientUS: BlobServiceClient; + private readonly blobServiceClientEU: BlobServiceClient; + private readonly blobServiceClientAU: BlobServiceClient; + + constructor() { + if ( + !process.env.STORAGE_ACCOUNT_ENDPOINT_US || + !process.env.STORAGE_ACCOUNT_ENDPOINT_AU || + !process.env.STORAGE_ACCOUNT_ENDPOINT_EU || + !process.env.STORAGE_ACCOUNT_NAME_US || + !process.env.STORAGE_ACCOUNT_KEY_US || + !process.env.STORAGE_ACCOUNT_NAME_AU || + !process.env.STORAGE_ACCOUNT_KEY_AU || + !process.env.STORAGE_ACCOUNT_NAME_EU || + !process.env.STORAGE_ACCOUNT_KEY_EU + ) { + throw new Error("Storage account information is missing"); + } + + // リージョンごとのSharedKeyCredentialを生成 + this.sharedKeyCredentialUS = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_US, + process.env.STORAGE_ACCOUNT_KEY_US + ); + this.sharedKeyCredentialAU = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_AU, + process.env.STORAGE_ACCOUNT_KEY_AU + ); + this.sharedKeyCredentialEU = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_EU, + process.env.STORAGE_ACCOUNT_KEY_EU + ); + + // Audioファイルの保存先のリージョンごとにBlobServiceClientを生成 + this.blobServiceClientUS = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_US, + this.sharedKeyCredentialUS + ); + this.blobServiceClientAU = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_AU, + this.sharedKeyCredentialAU + ); + this.blobServiceClientEU = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_EU, + this.sharedKeyCredentialEU + ); + } + + /** + * 指定されたファイルを削除します。 + * @param context + * @param accountId + * @param country + * @param fileName + * @returns file + */ + async deleteFile( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise { + context.log( + `[IN] ${this.deleteFile.name} | params: { ` + + `accountId: ${accountId} ` + + `country: ${country} ` + + `fileName: ${fileName} };` + ); + + try { + // 国に応じたリージョンでコンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient( + context, + accountId, + country + ); + // コンテナ内のBlobパス名を指定してClientを取得 + const blobClient = containerClient.getBlobClient(fileName); + + const { succeeded, errorCode, date } = await blobClient.deleteIfExists(); + context.log( + `succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}` + ); + + // 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする + // Blob不在の場合のエラーコードは「BlobNotFound」以下を参照 + // https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes + if (!succeeded && errorCode !== "BlobNotFound") { + throw new Error( + `delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}` + ); + } + } catch (e) { + context.error(`error=${e}`); + throw e; + } finally { + context.log(`[OUT] ${this.deleteFile.name}`); + } + } + + /** + * 指定してアカウントIDと国に応じたリージョンのコンテナクライアントを取得します。 + * @param context + * @param accountId + * @param country + * @returns + */ + private getContainerClient( + context: InvocationContext, + accountId: number, + country: string + ): ContainerClient { + context.log( + `[IN] ${this.getContainerClient.name} | params: { ` + + `accountId: ${accountId}; country: ${country} };` + ); + + const containerName = `account-${accountId}`; + if (BLOB_STORAGE_REGION_US.includes(country)) { + return this.blobServiceClientUS.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_AU.includes(country)) { + return this.blobServiceClientAU.getContainerClient(containerName); + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return this.blobServiceClientEU.getContainerClient(containerName); + } else { + throw new Error("invalid country"); + } + } +} diff --git a/dictation_function/src/blobstorage/blobstorage.service.ts b/dictation_function/src/blobstorage/blobstorage.service.ts new file mode 100644 index 0000000..8d6e4b7 --- /dev/null +++ b/dictation_function/src/blobstorage/blobstorage.service.ts @@ -0,0 +1,269 @@ +import { + BlobServiceClient, + StorageSharedKeyCredential, +} from "@azure/storage-blob"; +import { + IMPORT_USERS_CONTAINER_NAME, + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME, +} from "../constants"; +import { InvocationContext } from "@azure/functions"; + +export class BlobstorageService { + private readonly blobServiceClient: BlobServiceClient; + private readonly sharedKeyCredential: StorageSharedKeyCredential; + + constructor(useAnalysisBlob: boolean = false) { + if (!useAnalysisBlob) { + if ( + !process.env.STORAGE_ACCOUNT_NAME_IMPORT || + !process.env.STORAGE_ACCOUNT_KEY_IMPORT || + !process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT + ) { + throw new Error("Storage account information is missing"); + } + + this.sharedKeyCredential = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_IMPORT, + process.env.STORAGE_ACCOUNT_KEY_IMPORT + ); + this.blobServiceClient = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT, + this.sharedKeyCredential + ); + } else { + if ( + !process.env.STORAGE_ACCOUNT_NAME_ANALYSIS || + !process.env.STORAGE_ACCOUNT_KEY_ANALYSIS || + !process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS + ) { + throw new Error("Storage account information for analysis is missing"); + } + + this.sharedKeyCredential = new StorageSharedKeyCredential( + process.env.STORAGE_ACCOUNT_NAME_ANALYSIS, + process.env.STORAGE_ACCOUNT_KEY_ANALYSIS + ); + this.blobServiceClient = new BlobServiceClient( + process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS, + this.sharedKeyCredential + ); + } + } + /** + * Lists blobs + * @returns blobs + */ + async listBlobs(context: InvocationContext): Promise { + context.log(`[IN] ${this.listBlobs.name}`); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + + const blobNames: string[] = []; + // stage.json以外のファイルを取得 + for await (const blob of containerClient.listBlobsFlat({ + prefix: "U_", + })) { + blobNames.push(blob.name); + } + + return blobNames; + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.listBlobs.name}`); + } + } + + /** + * Downloads a blob + * @param context + * @param filename + * @returns file buffer + */ + public async downloadFileData( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.downloadFileData.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + try { + const downloadBlockBlobResponse = await blobClient.downloadToBuffer(); + return downloadBlockBlobResponse.toString(); + } catch (error) { + // ファイルが存在しない場合はundefinedを返す + if (error?.statusCode === 404) { + return undefined; + } + throw error; + } + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.downloadFileData.name}`); + } + } + + /** + * Updates file + * @param context + * @param filename + * @param data + * @returns file + */ + public async updateFile( + context: InvocationContext, + filename: string, + data: string + ): Promise { + context.log( + `[IN] ${this.updateFile.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + + const { response } = await containerClient.uploadBlockBlob( + filename, + data, + data.length + ); + if (response.errorCode) { + context.log(`update failed. response errorCode: ${response.errorCode}`); + return false; + } + return true; + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.updateFile.name}`); + } + } + + /** + * Deletes file + * @param context + * @param filename + * @returns file + */ + public async deleteFile( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.deleteFile.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + await blobClient.deleteIfExists(); + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.deleteFile.name}`); + } + } + + /** + * Determines whether file exists is + * @param context + * @param filename + * @returns file exists + */ + public async isFileExists( + context: InvocationContext, + filename: string + ): Promise { + context.log( + `[IN] ${this.isFileExists.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + IMPORT_USERS_CONTAINER_NAME + ); + const blobClient = containerClient.getBlobClient(filename); + return await blobClient.exists(); + } catch (error) { + context.error(error); + throw error; + } finally { + context.log(`[OUT] ${this.isFileExists.name}`); + } + } + + /** + * uplocad file analysis licenses csv + * @param context + * @param filename + * @param data + * @returns boolean + */ + public async uploadFileAnalysisLicensesCSV( + context: InvocationContext, + filename: string, + data: string + ): Promise { + context.log( + `[IN] ${this.uploadFileAnalysisLicensesCSV.name} | params: { filename: ${filename} }` + ); + try { + const containerClient = this.blobServiceClient.getContainerClient( + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME + ); + const { response } = await containerClient.uploadBlockBlob( + filename, + data, + data.length + ); + if (response.errorCode) { + context.log(`update failed. response errorCode: ${response.errorCode}`); + return false; + } + return true; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] ${this.uploadFileAnalysisLicensesCSV.name}`); + } + } + /** + * Create container analysis + * @param context + * @returns container + */ + async createContainerAnalysis(context: InvocationContext): Promise { + context.log(`[IN] ${this.createContainerAnalysis.name}`); + + try { + // コンテナ作成 + const containerClient = this.blobServiceClient.getContainerClient( + LICENSE_COUNT_ANALYSIS_CONTAINER_NAME + ); + // コンテナが存在しない場合のみ作成 + if (await containerClient.exists()) { + return; + } + await containerClient.create(); + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] ${this.createContainerAnalysis.name}`); + } + } +} diff --git a/dictation_function/src/blobstorage/types/guards.ts b/dictation_function/src/blobstorage/types/guards.ts new file mode 100644 index 0000000..c0bbbb0 --- /dev/null +++ b/dictation_function/src/blobstorage/types/guards.ts @@ -0,0 +1,165 @@ +import { InvocationContext } from "@azure/functions"; +import { IMPORT_USERS_STAGES } from "../../constants"; +import { ErrorRow, ImportData, ImportJson, StageJson } from "./types"; + +const isErrorRow = (obj: any): obj is ErrorRow => { + if (typeof obj !== "object") return false; + const errorRow = obj as ErrorRow; + if (errorRow.name === undefined || typeof errorRow.name !== "string") { + return false; + } + if (errorRow.row === undefined || typeof errorRow.row !== "number") { + return false; + } + if (errorRow.error === undefined || typeof errorRow.error !== "string") { + return false; + } + return true; +}; + +export const isStageJson = (obj: any): obj is StageJson => { + if (typeof obj !== "object") return false; + const stageJson = obj as StageJson; + if ( + stageJson.filename !== undefined && + typeof stageJson.filename !== "string" + ) { + return false; + } + if (stageJson.update === undefined || typeof stageJson.update !== "number") { + return false; + } + if (stageJson.row !== undefined && typeof stageJson.row !== "number") { + return false; + } + + if ( + stageJson.errors !== undefined && + (!Array.isArray(stageJson.errors) || + !stageJson.errors.every((x) => isErrorRow(x))) + ) { + return false; + } + if ( + stageJson.state === undefined || + !Object.values(IMPORT_USERS_STAGES).includes(stageJson.state) + ) { + return false; + } + return true; +}; + +const isImportData = ( + context: InvocationContext, + obj: any +): obj is ImportData => { + if (typeof obj !== "object") return false; + const importData = obj as ImportData; + if (importData.name === undefined || typeof importData.name !== "string") { + context.log("name is missing or not a string"); + return false; + } + if (importData.email === undefined || typeof importData.email !== "string") { + context.log("email is missing or not a string"); + return false; + } + if (importData.role === undefined || typeof importData.role !== "number") { + context.log("role is missing or not a number"); + return false; + } + if ( + importData.author_id !== undefined && + typeof importData.author_id !== "string" + ) { + context.log("author_id is missing or not a string"); + return false; + } + if ( + importData.auto_renew === undefined || + typeof importData.auto_renew !== "number" + ) { + context.log("auto_renew is missing or not a number"); + return false; + } + if ( + importData.notification === undefined || + typeof importData.notification !== "number" + ) { + context.log("notification is missing or not a number"); + return false; + } + if ( + importData.encryption !== undefined && + typeof importData.encryption !== "number" + ) { + context.log("encryption is missing or not a number"); + return false; + } + if ( + importData.encryption_password !== undefined && + typeof importData.encryption_password !== "string" + ) { + context.log("encryption_password is missing or not a string"); + return false; + } + if ( + importData.prompt !== undefined && + typeof importData.prompt !== "number" + ) { + context.log("prompt is missing or not a number"); + return false; + } + return true; +}; + +export const isImportJson = ( + context: InvocationContext, + obj: any +): obj is ImportJson => { + if (typeof obj !== "object") return false; + const importJson = obj as ImportJson; + if ( + importJson.account_id === undefined || + typeof importJson.account_id !== "number" + ) { + context.log("account_id is missing or not a number"); + return false; + } + if ( + importJson.user_id === undefined || + typeof importJson.user_id !== "number" + ) { + context.log("user_id is missing or not a number"); + return false; + } + if ( + importJson.user_role === undefined || + typeof importJson.user_role !== "string" + ) { + context.log("user_role is missing or not a string"); + return false; + } + if ( + importJson.external_id === undefined || + typeof importJson.external_id !== "string" + ) { + context.log("external_id is missing or not a string"); + return false; + } + if ( + importJson.file_name === undefined || + typeof importJson.file_name !== "string" + ) { + context.log("file_name is missing or not a string"); + return false; + } + if ( + importJson.data === undefined || + !Array.isArray(importJson.data) || + !importJson.data.every((x) => isImportData(context, x)) + ) { + context.log("data is missing or not an array of ImportData"); + return false; + } + return true; +}; diff --git a/dictation_function/src/blobstorage/types/types.ts b/dictation_function/src/blobstorage/types/types.ts new file mode 100644 index 0000000..97ba857 --- /dev/null +++ b/dictation_function/src/blobstorage/types/types.ts @@ -0,0 +1,54 @@ +import { IMPORT_USERS_STAGES, USER_ROLES } from "../../constants"; + +export type StageType = + (typeof IMPORT_USERS_STAGES)[keyof typeof IMPORT_USERS_STAGES]; + +export type StageJson = { + filename?: string | undefined; + update: number; + row?: number | undefined; + errors?: ErrorRow[] | undefined; + state: StageType; +}; + +export type ErrorRow = { + name: string; + row: number; + error: string; +}; + +export type ImportJson = { + account_id: number; + user_id: number; + user_role: RoleType; + external_id: string; + file_name: string; + date: number; + data: ImportData[]; +}; + +export type ImportData = { + name: string; + email: string; + role: number; + author_id?: string | undefined; + auto_renew: number; + notification: number; + encryption?: number | undefined; + encryption_password?: string | undefined; + prompt?: number | undefined; +}; + +export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES]; + +export type User = { + name: string; + role: RoleType; + email: string; + autoRenew: boolean; + notification: boolean; + authorId?: string | undefined; + encryption?: boolean | undefined; + encryptionPassword?: string | undefined; + prompt?: boolean | undefined; +}; diff --git a/dictation_function/src/common/errors/code.ts b/dictation_function/src/common/errors/code.ts new file mode 100644 index 0000000..25366a9 --- /dev/null +++ b/dictation_function/src/common/errors/code.ts @@ -0,0 +1,79 @@ +/* +エラーコード作成方針 +E+6桁(数字)で構成する。 +- 1~2桁目の値は種類(業務エラー、システムエラー...) +- 3~4桁目の値は原因箇所(トークン、DB、...) +- 5~6桁目の値は任意の重複しない値 +ex) +E00XXXX : システムエラー(通信エラーやDB接続失敗など) +E01XXXX : 業務エラー +EXX00XX : 内部エラー(内部プログラムのエラー) +EXX01XX : トークンエラー(トークン認証関連) +EXX02XX : DBエラー(DB関連) +EXX03XX : ADB2Cエラー(DB関連) +*/ +export const errorCodes = [ + "E009999", // 汎用エラー + "E000101", // トークン形式不正エラー + "E000102", // トークン有効期限切れエラー + "E000103", // トークン非アクティブエラー + "E000104", // トークン署名エラー + "E000105", // トークン発行元エラー + "E000106", // トークンアルゴリズムエラー + "E000107", // トークン不足エラー + "E000108", // トークン権限エラー + "E000301", // ADB2Cへのリクエスト上限超過エラー + "E010001", // パラメータ形式不正エラー + "E010201", // 未認証ユーザエラー + "E010202", // 認証済ユーザエラー + "E010203", // 管理ユーザ権限エラー + "E010204", // ユーザ不在エラー + "E010205", // DBのRoleが想定外の値エラー + "E010206", // DBのTierが想定外の値エラー + "E010207", // ユーザーのRole変更不可エラー + "E010208", // ユーザーの暗号化パスワード不足エラー + "E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー + "E010301", // メールアドレス登録済みエラー + "E010302", // authorId重複エラー + "E010401", // PONumber重複エラー + "E010501", // アカウント不在エラー + "E010502", // アカウント情報変更不可エラー + "E010503", // 代行操作不許可エラー + "E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない) + "E010602", // タスク変更権限不足エラー + "E010603", // タスク不在エラー + "E010701", // Blobファイル不在エラー + "E010801", // ライセンス不在エラー + "E010802", // ライセンス取り込み済みエラー + "E010803", // ライセンス発行済みエラー + "E010804", // ライセンス数不足エラー + "E010805", // ライセンス有効期限切れエラー + "E010806", // ライセンス割り当て不可エラー + "E010807", // ライセンス割り当て解除不可エラー + "E010808", // ライセンス注文キャンセル不可エラー + "E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合) + "E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合) + "E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合) + "E010908", // タイピストグループ不在エラー + "E010909", // タイピストグループ名重複エラー + "E011001", // ワークタイプ重複エラー + "E011002", // ワークタイプ登録上限超過エラー + "E011003", // ワークタイプ不在エラー + "E011004", // ワークタイプ使用中エラー + "E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー + "E013002", // ワークフロー不在エラー + "E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + "E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + "E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + "E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + "E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + "E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + "E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + "E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている) + "E015001", // タイピストグループ削除済みエラー + "E015002", // タイピストグループがワークフローに紐づいているエラー + "E015003", // タイピストグループがルーティングされているエラー + "E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + "E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + "E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) +] as const; diff --git a/dictation_function/src/common/errors/index.ts b/dictation_function/src/common/errors/index.ts new file mode 100644 index 0000000..1543e4c --- /dev/null +++ b/dictation_function/src/common/errors/index.ts @@ -0,0 +1,3 @@ +export * from "./code"; +export * from "./types"; +export * from "./utils"; diff --git a/dictation_function/src/common/errors/types.ts b/dictation_function/src/common/errors/types.ts new file mode 100644 index 0000000..8cc801e --- /dev/null +++ b/dictation_function/src/common/errors/types.ts @@ -0,0 +1,9 @@ +import { errorCodes } from "./code"; + +export type ErrorObject = { + message: string; + code: ErrorCodeType; + statusCode?: number; +}; + +export type ErrorCodeType = typeof errorCodes[number]; diff --git a/dictation_function/src/common/errors/utils.ts b/dictation_function/src/common/errors/utils.ts new file mode 100644 index 0000000..3dd2410 --- /dev/null +++ b/dictation_function/src/common/errors/utils.ts @@ -0,0 +1,101 @@ +import { AxiosError } from "axios"; +import { isError } from "lodash"; +import { ErrorResponse } from "../../api"; +import { errorCodes } from "./code"; +import { ErrorCodeType, ErrorObject } from "./types"; + +export const createErrorObject = (error: unknown): ErrorObject => { + // 最低限通常のエラーかを判定 + // Error以外のものがthrowされた場合 + // 基本的にないはずだがプログラム上あるので拾う + if (!isError(error)) { + return { + message: "not error type.", + code: "E009999", + }; + } + + // Axiosエラー 通信してのエラーであるかを判定 + if (!isAxiosError(error)) { + return { + message: "not axios error.", + code: "E009999", + }; + } + + const errorResponse = error.response; + if (!errorResponse) { + return { + message: error.message, + code: "E009999", + statusCode: errorResponse, + }; + } + + const { data } = errorResponse; + + // 想定しているエラーレスポンスの型か判定 + if (!isErrorResponse(data)) { + return { + message: error.message, + code: "E009999", + statusCode: errorResponse.status, + }; + } + + const { message, code } = data; + + // 想定しているエラーコードかを判定 + if (!isErrorCode(code)) { + return { + message, + code: "E009999", + statusCode: errorResponse.status, + }; + } + + return { + message, + code, + statusCode: errorResponse.status, + }; +}; + +const isAxiosError = (e: unknown): e is AxiosError => { + const error = e as AxiosError; + return error?.isAxiosError ?? false; +}; + +const isErrorResponse = (error: unknown): error is ErrorResponse => { + const errorResponse = error as ErrorResponse; + if ( + errorResponse === undefined || + errorResponse.message === undefined || + errorResponse.code === undefined + ) { + return false; + } + + return true; +}; + +const isErrorCode = (errorCode: string): errorCode is ErrorCodeType => + errorCodes.includes(errorCode as ErrorCodeType); + +export const isErrorObject = ( + data: unknown +): data is { error: ErrorObject } => { + if ( + data && + typeof data === "object" && + "error" in data && + typeof (data as { error: ErrorObject }).error === "object" && + typeof (data as { error: ErrorObject }).error.message === "string" && + typeof (data as { error: ErrorObject }).error.code === "string" && + (typeof (data as { error: ErrorObject }).error.statusCode === "number" || + (data as { error: ErrorObject }).error.statusCode === undefined) + ) { + return true; + } + return false; +}; diff --git a/dictation_function/src/common/jwt/index.ts b/dictation_function/src/common/jwt/index.ts new file mode 100644 index 0000000..60cc8f7 --- /dev/null +++ b/dictation_function/src/common/jwt/index.ts @@ -0,0 +1,3 @@ +import { isVerifyError, sign, verify, decode, getJwtKey } from "./jwt"; + +export { isVerifyError, sign, verify, decode, getJwtKey }; diff --git a/dictation_function/src/common/jwt/jwt.spec.ts b/dictation_function/src/common/jwt/jwt.spec.ts new file mode 100644 index 0000000..b58d91c --- /dev/null +++ b/dictation_function/src/common/jwt/jwt.spec.ts @@ -0,0 +1,250 @@ +import { sign, verify, isVerifyError } from "./jwt"; +import base64url from "base64url"; + +test("success sign and verify", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const payload = verify<{ value: "testvalue" }>(token, publicKey); + if (isVerifyError(payload)) { + throw new Error(`${payload.reason} | ${payload.message}`); + } + + expect(payload.value).toBe("testvalue"); +}); + +test("failed sign and verify (jwt expired)", () => { + // 有効期限を0秒にすることで、検証を行った時点で有効期限切れにする + const token = sign({ value: "testvalue" }, 0, privateKey); + const payload = verify<{ value: "testvalue" }>(token, publicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("ExpiredError"); +}); + +test("failed sign and verify (invalid key pair)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + // 秘密鍵と対ではない公開鍵を使用して検証する + const payload = verify<{ value: "testvalue" }>(token, anotherPublicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("InvalidToken"); + expect(payload.message).toBe("invalid signature"); +}); + +test("failed sign and verify (invalid public key)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + // 公開鍵の形式になっていない文字列を使用して検証する + const payload = verify<{ value: "testvalue" }>(token, fakePublicKey); + if (!isVerifyError(payload)) { + throw new Error(JSON.stringify(payload)); + } + expect(payload.reason).toBe("InvalidToken"); + expect(payload.message).toBe( + "secretOrPublicKey must be an asymmetric key when using RS256" + ); +}); + +test("failed sign (invalid private key)", () => { + expect(() => { + // 不正な秘密鍵で署名しようとする場合はエラーがthrowされる + sign({ value: "testvalue" }, 5 * 60, fakePrivateKey); + }).toThrowError(); +}); + +test("success rewrite-token verify (as is)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { header, payload, verifySignature } = splitToken(token); + + { + // 何も操作せずに構築しなおした場合、成功する + const validToken = rebuildToken(header, payload, verifySignature); + + const value = verify<{ value: string }>(validToken, publicKey); + if (isVerifyError(value)) { + throw new Error(`${value.reason} | ${value.message}`); + } + + expect(value.value).toBe("testvalue"); + } +}); + +test("failed rewrite-token verify (override algorithm)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { payload, verifySignature } = splitToken(token); + + { + // 検証アルゴリズムを「検証なし」に書き換える + const headerObject = { alg: "none" }; + const payloadObject = JSON.parse(payload) as { + value: string; + iat: number; + exp: number; + }; + + // 内容を操作して構築しなおした場合、失敗する + const customToken = rebuildToken( + JSON.stringify(headerObject), + JSON.stringify(payloadObject), + verifySignature + ); + + const value = verify<{ value: string }>(customToken, publicKey); + if (!isVerifyError(value)) { + throw new Error(JSON.stringify(payload)); + } + expect(value.reason).toBe("InvalidToken"); + expect(value.message).toBe("invalid algorithm"); + } +}); + +test("failed rewrite-token verify (override expire)", () => { + const token = sign({ value: "testvalue" }, 5 * 60, privateKey); + const { header, payload, verifySignature } = splitToken(token); + + { + // expの値を操作する + const payloadObject = JSON.parse(payload) as { + value: string; + iat: number; + exp: number; + }; + payloadObject.exp = payloadObject.exp + 100000; + + // 内容を操作して構築しなおした場合、失敗する + const customToken = rebuildToken( + header, + JSON.stringify(payloadObject), + verifySignature + ); + + const value = verify<{ value: string }>(customToken, publicKey); + if (!isVerifyError(value)) { + throw new Error(JSON.stringify(payload)); + } + expect(value.reason).toBe("InvalidToken"); + expect(value.message).toBe("invalid signature"); + } +}); + +// JWT改竄テスト用ユーティリティ +const splitToken = ( + token: string +): { header: string; payload: string; verifySignature: string } => { + const splited = token.split("."); + + const header = base64url.decode(splited[0]); + const payload = base64url.decode(splited[1]); + const verifySignature = splited[2]; + return { header, payload, verifySignature }; +}; + +// JWT改竄テスト用ユーティリティ +const rebuildToken = ( + header: string, + payload: string, + verifySignature: string +): string => { + const rebuild_header = base64url.encode(header); + const rebuild_payload = base64url.encode(payload); + return `${rebuild_header}.${rebuild_payload}.${verifySignature}`; +}; + +// テスト用に生成した秘密鍵 +const privateKey = [ + "-----BEGIN RSA PRIVATE KEY-----", + "MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F", + "GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a", + "fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa", + "FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq", + "zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF", + "+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN", + "LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq", + "L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0", + "WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE", + "x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm", + "+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX", + "cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc", + "BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H", + "IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+", + "y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb", + "iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9", + "BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95", + "mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/", + "O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s", + "xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust", + "nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT", + "tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK", + "YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM", + "iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk", + "JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==", + "-----END RSA PRIVATE KEY-----", +].join("\n"); + +// テスト用に生成した公開鍵 +const publicKey = [ + "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD", + "jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23", + "tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5", + "7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD", + "alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2", + "1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a", + "IQIDAQAB", + "-----END PUBLIC KEY-----", +].join("\n"); + +// テスト用に作成した、違う秘密鍵から生成した公開鍵 +const anotherPublicKey = [ + "-----BEGIN PUBLIC KEY-----", + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3", + "2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig", + "n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9", + "RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk", + "EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x", + "2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81", + "KQIDAQAB", + "-----END PUBLIC KEY-----", +].join("\n"); + +// 秘密鍵のように見えるが想定する形式と違う +const fakePrivateKey = [ + "-----BAGIN RSA PRIVATE KEY-----", + "MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F", + "GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a", + "fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa", + "FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq", + "zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF", + "+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN", + "LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq", + "L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0", + "WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE", + "x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm", + "+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX", + "cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc", + "BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H", + "IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+", + "y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb", + "iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9", + "BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95", + "mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/", + "O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s", + "xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust", + "nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT", + "tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK", + "YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM", + "iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk", + "JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==", + "-----END RSA PRIVATE KEY-----", +].join("\n"); + +// 公開鍵のように見えるが想定する形式と違う +const fakePublicKey = [ + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3", + "2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig", + "n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9", + "RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk", + "EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x", + "2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81", + "KQIDAQAB", +].join("\n"); diff --git a/dictation_function/src/common/jwt/jwt.ts b/dictation_function/src/common/jwt/jwt.ts new file mode 100644 index 0000000..faf9f85 --- /dev/null +++ b/dictation_function/src/common/jwt/jwt.ts @@ -0,0 +1,130 @@ +import * as jwt from "jsonwebtoken"; +// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる? +import { decode as jwtDecode } from "jsonwebtoken"; + +export type VerifyError = { + reason: "ExpiredError" | "InvalidToken" | "InvalidTimeStamp" | "Unknown"; + message: string; +}; + +export const isVerifyError = (arg: unknown): arg is VerifyError => { + const value = arg as VerifyError; + if (value.message === undefined) { + return false; + } + + if (value.reason === undefined) { + return false; + } + switch (value.reason) { + case "ExpiredError": + case "InvalidTimeStamp": + case "InvalidToken": + case "Unknown": + return true; + default: + return false; + } +}; + +/** + * Payloadと秘密鍵を使用して署名されたJWTを生成します + * @param {T} payload payloadの型 + * @param {number} expirationSeconds トークンの有効期限(秒) + * @param {string} privateKey 署名に使用する秘密鍵 + * @return {string} 署名済みトークン + * @throws {Error} 秘密鍵の形式が間違っている等の理由が格納されたErrorオブジェクト + */ +export const sign = ( + payload: T, + expirationSeconds: number, + privateKey: string +): string => { + try { + const token = jwt.sign(payload, privateKey, { + expiresIn: expirationSeconds, + algorithm: "RS256", + }); + return token; + } catch (e) { + throw e; + } +}; + +/** + * tokenと公開鍵を使用して検証済みJWTのpayloadを取得します + * @param {string} token JWT + * @param {string} publicKey 検証に使用する公開鍵 + * @return {T | VerifyError} Payload または 検証エラーの内容を表すオブジェクト + */ +export const verify = ( + token: string, + publicKey: string +): T | VerifyError => { + try { + const payload = jwt.verify(token, publicKey, { + algorithms: ["RS256"], + }) as T; + return payload; + } catch (e) { + if (e instanceof jwt.TokenExpiredError) { + return { + reason: "ExpiredError", + message: e.message, + }; + } else if (e instanceof jwt.NotBeforeError) { + return { + reason: "InvalidTimeStamp", + message: e.message, + }; + } else if (e instanceof jwt.JsonWebTokenError) { + return { + reason: "InvalidToken", + message: e.message, + }; + } else { + return { + reason: "Unknown", + message: e.message, + }; + } + } +}; + +/** + * tokenから未検証のJWTのpayloadを取得します + * @param {string} token JWT + * @return {T | VerifyError} Payload または デコードエラーの内容を表すオブジェクト + */ +export const decode = (token: string): T | VerifyError => { + try { + const payload = jwtDecode(token, { + json: true, + }) as T; + return payload; + } catch (e) { + if (e instanceof jwt.TokenExpiredError) { + return { + reason: "ExpiredError", + message: e.message, + }; + } else if (e instanceof jwt.NotBeforeError) { + return { + reason: "InvalidTimeStamp", + message: e.message, + }; + } else if (e instanceof jwt.JsonWebTokenError) { + return { + reason: "InvalidToken", + message: e.message, + }; + } else { + return { + reason: "Unknown", + message: e.message, + }; + } + } +}; + +export const getJwtKey = (key: string): string => key.replace(/\\n/g, "\n"); diff --git a/dictation_function/src/common/jwt/types.ts b/dictation_function/src/common/jwt/types.ts new file mode 100644 index 0000000..7ae28d2 --- /dev/null +++ b/dictation_function/src/common/jwt/types.ts @@ -0,0 +1,32 @@ +export type AccessToken = { + /** + * 外部認証サービスの識別子(代行者) + */ + delegateUserId?: string | undefined; + /** + * 外部認証サービスの識別子 + */ + userId: string; + /** + * 半角スペース区切りのRoleを表現する文字列(ex. "author admin") + */ + role: string; + /** + * アカウントの階層情報(1~5までの半角数字) + */ + tier: number; +}; + +// システムの内部で発行し、外部に公開しないトークン +// システム間通信用(例: Azure Functions→AppService)に使用する +export type SystemAccessToken = { + /** + * トークンの発行者名(ログ記録用) + */ + systemName: string; + + /** + * 付加情報を 文字情報として格納できる + */ + context?: string; +}; diff --git a/dictation_function/src/constants/index.ts b/dictation_function/src/constants/index.ts index 3975fda..6c8c0e8 100644 --- a/dictation_function/src/constants/index.ts +++ b/dictation_function/src/constants/index.ts @@ -294,6 +294,121 @@ export const HTTP_STATUS_CODES = { INTERNAL_SERVER_ERROR: 500, }; +/** + * ユーザー一括登録用のBlobコンテナ名 + * @const {string} + */ +export const IMPORT_USERS_CONTAINER_NAME = "import-users"; + +/** + * ユーザー一括登録の最大処理時間(分) + * @const {number} + */ +export const IMPORT_USERS_MAX_DURATION_MINUTES = 30; + +/** + * ユーザー一括登録のステージ管理ファイル名 + * @const {string} + */ +export const IMPORT_USERS_STAGE_FILE_NAME = "stage.json"; + +/** + * ユーザー一括登録のステージ管理のステージ + * @const {string} + */ +export const IMPORT_USERS_STAGES = { + CREATED: "created", + PRAPARE: "prepare", + START: "start", + COMPLETE: "complete", + DONE: "done", +} as const; + +/** + * ユーザーのロールと数値のマッピング + * @const {string} + */ +export const RoleNumberMap: Record = { + 0: USER_ROLES.NONE, + 1: USER_ROLES.AUTHOR, + 2: USER_ROLES.TYPIST, +} as const; + +export const SYSTEM_IMPORT_USERS = "import-users"; + +export const ROW_START_INDEX = 2; + +/** + * ファイル保持日数の初期値 + * @const {number} + */ +export const FILE_RETENTION_DAYS_DEFAULT = 30; + +/** + * ライセンス数推移出力機能のCSVヘッダ + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_HEADER = { + ACCOUNT: "アカウント", + TARGET_YEAE_AND_MONTH: "対象年月", + CATEGORY_1: "カテゴリー1", + CATEGORY_2: "カテゴリー2", + LICENSE_TYPE: "ライセンス種別", + ROLE: "役割", + COUNT: "数量", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(カテゴリー1) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_CATEGORY_1 = { + VALID_LICENSES: "有効ライセンス数", + NEW_ISSUE_LICENSES: "新規発行ライセンス数", + INVALID_LICENSES: "失効ライセンス数", + SWICHED_LICENSES: "有効ライセンス切り替え", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(カテゴリー2) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_CATEGORY_2 = { + OWNER_LICENSES: "所有ライセンス数", + IN_USE_LICENSES: "使用中ライセンス数", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(ライセンス種別) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_LICENSE_TYPE = { + TRIAL: "Trial", + STANDARD: "Standard", + CARD: "Card", + SWITCH_FROM_TRIAL: "トライアルから切り替え", + SWITCH_FROM_CARD: "カードから切り替え", +}; +/** + * ライセンス数推移出力機能のCSV項目で使用する日本語(役割) + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_ROLE = { + AUTHOR: "Author", + TYPIST: "Typist", + NONE: "None", + UNALLOCATED: "Unallocated", +}; + +/** + * ライセンス数推移出力機能のファイルの先頭文字列 + * @const {string[]} + */ +export const LICENSE_COUNT_ANALYSIS_FRONT_STRING = "LicenseAggregated"; + +/** + * ライセンス数推移CSV用のコンテナー名 + * @const {string} + */ +export const LICENSE_COUNT_ANALYSIS_CONTAINER_NAME = "analysis-licenses"; + /** * メールの置換文字列 * @const {string} @@ -302,4 +417,4 @@ export const CUSTOMER_NAME = "$CUSTOMER_NAME$"; export const DEALER_NAME = "$DEALER_NAME$"; export const TOP_URL = "$TOP_URL$"; export const USER_NAME = "$USER_NAME$"; -export const USER_EMAIL = "$USER_EMAIL$"; \ No newline at end of file +export const USER_EMAIL = "$USER_EMAIL$"; diff --git a/dictation_function/src/database/initializeDataSource.ts b/dictation_function/src/database/initializeDataSource.ts new file mode 100644 index 0000000..f323d54 --- /dev/null +++ b/dictation_function/src/database/initializeDataSource.ts @@ -0,0 +1,47 @@ +import { User, UserArchive } from "../entity/user.entity"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { + License, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + LicenseArchive, +} from "../entity/license.entity"; +import { InvocationContext } from "@azure/functions"; +import { DataSource } from "typeorm"; +import { Task } from "../entity/task.entity"; +import { AudioFile } from "../entity/audio_file.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; + +export async function initializeDataSource( + context: InvocationContext +): Promise { + try { + const datasource = new DataSource({ + type: "mysql", + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME_CCB, + entities: [ + User, + UserArchive, + Account, + AccountArchive, + Task, + AudioFile, + AudioOptionItem, + License, + LicenseArchive, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + ], + }); + await datasource.initialize(); + return datasource; + } catch (e) { + context.log("Database initialize failed."); + context.error(e); + throw e; + } +} diff --git a/dictation_function/src/entity/account.entity.ts b/dictation_function/src/entity/account.entity.ts index 15988a6..43b67ab 100644 --- a/dictation_function/src/entity/account.entity.ts +++ b/dictation_function/src/entity/account.entity.ts @@ -1,5 +1,6 @@ import { bigintTransformer } from "../common/entity"; -import { User } from "./user.entity"; +import { User, UserArchive } from "./user.entity"; +import { License, LicenseArchive } from "./license.entity"; import { Entity, Column, @@ -10,6 +11,7 @@ import { JoinColumn, OneToMany, } from "typeorm"; +import { FILE_RETENTION_DAYS_DEFAULT } from "../constants"; @Entity({ name: "accounts" }) export class Account { @@ -46,6 +48,12 @@ export class Account { @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) active_worktype_id: number | null; + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: FILE_RETENTION_DAYS_DEFAULT }) + file_retention_days: number; + @Column({ nullable: true, type: "datetime" }) deleted_at: Date | null; @@ -75,6 +83,86 @@ export class Account { @JoinColumn({ name: "secondary_admin_user_id" }) secondaryAdminUser: User | null; - @OneToMany(() => User, (user) => user.id) + @OneToMany(() => User, (user) => user.account) + @JoinColumn({ name: "id" }) user: User[] | null; + + @OneToMany(() => UserArchive, (userArchive) => userArchive.account) + @JoinColumn({ name: "id" }) + userArchive: UserArchive[] | null; + + @OneToMany(() => License, (license) => license.account) + licenses: License[] | null; +} + +@Entity({ name: "accounts_archive" }) +export class AccountArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + parent_account_id: number | null; + + @Column() + tier: number; + + @Column() + country: string; + + @Column({ default: false }) + delegation_permission: boolean; + + @Column({ default: false }) + locked: boolean; + + @Column({ default: false }) + verified: boolean; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + primary_admin_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + secondary_admin_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + active_worktype_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.id) + @JoinColumn({ name: "primary_admin_user_id" }) + primaryAdminUser: UserArchive | null; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.id) + @JoinColumn({ name: "secondary_admin_user_id" }) + secondaryAdminUser: UserArchive | null; + + @OneToMany(() => UserArchive, (userArchive) => userArchive.account) + @JoinColumn({ name: "id" }) + userArchive: UserArchive[] | null; + + @OneToMany( + () => LicenseArchive, + (licenseArchive) => licenseArchive.accountArchive + ) + licensesArchive: LicenseArchive[] | null; } diff --git a/dictation_function/src/entity/audio_file.entity.ts b/dictation_function/src/entity/audio_file.entity.ts new file mode 100644 index 0000000..00f3ea2 --- /dev/null +++ b/dictation_function/src/entity/audio_file.entity.ts @@ -0,0 +1,42 @@ +import { Entity, Column, PrimaryGeneratedColumn, OneToOne } from "typeorm"; + +@Entity({ name: "audio_files" }) +export class AudioFile { + @PrimaryGeneratedColumn() + id: number; + + @Column() + account_id: number; + @Column() + owner_user_id: number; + @Column() + url: string; + @Column() + file_name: string; + @Column() + raw_file_name: string; + @Column() + author_id: string; + @Column() + work_type_id: string; + @Column() + started_at: Date; + @Column({ type: "time" }) + duration: string; + @Column() + finished_at: Date; + @Column() + uploaded_at: Date; + @Column() + file_size: number; + @Column() + priority: string; + @Column() + audio_format: string; + @Column({ nullable: true, type: "varchar" }) + comment: string | null; + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + @Column() + is_encrypted: boolean; +} diff --git a/dictation_function/src/entity/audio_option_item.entity.ts b/dictation_function/src/entity/audio_option_item.entity.ts new file mode 100644 index 0000000..a3fe3cc --- /dev/null +++ b/dictation_function/src/entity/audio_option_item.entity.ts @@ -0,0 +1,13 @@ +import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; + +@Entity({ name: "audio_option_items" }) +export class AudioOptionItem { + @PrimaryGeneratedColumn() + id: number; + @Column() + audio_file_id: number; + @Column() + label: string; + @Column() + value: string; +} diff --git a/dictation_function/src/entity/license.entity.ts b/dictation_function/src/entity/license.entity.ts index ca38f5a..c054336 100644 --- a/dictation_function/src/entity/license.entity.ts +++ b/dictation_function/src/entity/license.entity.ts @@ -9,8 +9,8 @@ import { ManyToOne, } from "typeorm"; import { bigintTransformer } from "../common/entity"; -import { User } from "./user.entity"; - +import { User, UserArchive } from "./user.entity"; +import { Account, AccountArchive } from "./account.entity"; @Entity({ name: "licenses" }) export class License { @PrimaryGeneratedColumn() @@ -61,6 +61,10 @@ export class License { @OneToOne(() => User, (user) => user.license) @JoinColumn({ name: "allocated_user_id" }) user: User | null; + + @ManyToOne(() => Account, (account) => account.licenses) + @JoinColumn({ name: "account_id" }) + account: Account | null; } @Entity({ name: "license_allocation_history" }) @@ -112,4 +116,125 @@ export class LicenseAllocationHistory { }) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 @JoinColumn({ name: "license_id" }) license: License | null; + + @ManyToOne(() => User, (user) => user.licenseAllocationHistory) // Userエンティティとの関連を設定 + @JoinColumn({ name: "user_id" }) // user_idを外部キーとして使用 + user: User; +} + +@Entity({ name: "licenses_archive" }) +export class LicenseArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: "datetime" }) + expiry_date: Date | null; + + @Column() + account_id: number; + + @Column() + type: string; + + @Column() + status: string; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + allocated_user_id: number | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + order_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + delete_order_id: number | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + updated_at: Date; + + @OneToOne(() => UserArchive, (userArchive) => userArchive.licenseArchive) + @JoinColumn({ name: "allocated_user_id" }) + userArchive: UserArchive | null; + + @ManyToOne( + () => AccountArchive, + (accountArchive) => accountArchive.licensesArchive + ) + @JoinColumn({ name: "account_id" }) + accountArchive: AccountArchive | null; +} + +@Entity({ name: "license_allocation_history_archive" }) +export class LicenseAllocationHistoryArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column() + user_id: number; + + @Column() + license_id: number; + + @Column() + is_allocated: boolean; + + @Column() + account_id: number; + + @Column() + executed_at: Date; + + @Column() + switch_from_type: string; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) + updated_at: Date; + + @ManyToOne(() => LicenseArchive, (licensesArchive) => licensesArchive.id, { + createForeignKeyConstraints: false, + }) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "license_id" }) + license: LicenseArchive | null; + + @ManyToOne( + () => UserArchive, + (userArchive) => userArchive.licenseAllocationHistoryArchive + ) // Userエンティティとの関連を設定 + @JoinColumn({ name: "user_id" }) // user_idを外部キーとして使用 + userArchive: UserArchive; } diff --git a/dictation_function/src/entity/task.entity.ts b/dictation_function/src/entity/task.entity.ts new file mode 100644 index 0000000..ceb96fc --- /dev/null +++ b/dictation_function/src/entity/task.entity.ts @@ -0,0 +1,59 @@ +import { AudioOptionItem } from "./audio_option_item.entity"; +import { AudioFile } from "./audio_file.entity"; +import { + Entity, + Column, + PrimaryGeneratedColumn, + OneToOne, + JoinColumn, + OneToMany, + ManyToOne, + CreateDateColumn, + UpdateDateColumn, +} from "typeorm"; +import { bigintTransformer } from "../common/entity"; +import { Account } from "./account.entity"; + +@Entity({ name: "tasks" }) +export class Task { + @PrimaryGeneratedColumn() + id: number; + @Column() + job_number: string; + @Column() + account_id: number; + @Column({ nullable: true, type: "boolean" }) + is_job_number_enabled: boolean | null; + @Column() + audio_file_id: number; + @Column() + status: string; + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + typist_user_id: number | null; + @Column() + priority: string; + @Column({ nullable: true, type: "bigint", transformer: bigintTransformer }) + template_file_id: number | null; + @Column({ nullable: true, type: "datetime" }) + started_at: Date | null; + @Column({ nullable: true, type: "datetime" }) + finished_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; +} diff --git a/dictation_function/src/entity/user.entity.ts b/dictation_function/src/entity/user.entity.ts index d8c78f9..44a6940 100644 --- a/dictation_function/src/entity/user.entity.ts +++ b/dictation_function/src/entity/user.entity.ts @@ -7,9 +7,10 @@ import { OneToOne, JoinColumn, ManyToOne, + OneToMany, } from "typeorm"; -import { License } from "./license.entity"; -import { Account } from "./account.entity"; +import { License, LicenseAllocationHistory, LicenseArchive, LicenseAllocationHistoryArchive } from "./license.entity"; +import { Account, AccountArchive } from "./account.entity"; @Entity({ name: "users" }) export class User { @@ -49,9 +50,6 @@ export class User { @Column({ default: false }) encryption: boolean; - @Column({ nullable: true, type: "varchar" }) - encryption_password: string | null; - @Column({ default: false }) prompt: boolean; @@ -84,4 +82,104 @@ export class User { @OneToOne(() => License, (license) => license.user) license: License | null; + + @OneToMany( + () => LicenseAllocationHistory, + (licenseAllocationHistory) => licenseAllocationHistory.user + ) + licenseAllocationHistory: LicenseAllocationHistory[] | null; +} + + +@Entity({ name: "users_archive" }) +export class UserArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column() + external_id: string; + + @Column() + account_id: number; + + @Column() + role: string; + + @Column({ nullable: true, type: "varchar" }) + author_id: string | null; + + @Column({ nullable: true, type: "varchar" }) + accepted_eula_version: string | null; + + @Column({ nullable: true, type: "varchar" }) + accepted_dpa_version: string | null; + + @Column({ default: false }) + email_verified: boolean; + + @Column({ default: true }) + auto_renew: boolean; + + @Column({ default: true }) + notification: boolean; + + @Column({ default: false }) + encryption: boolean; + + @Column({ default: false }) + prompt: boolean; + + @Column({ nullable: true, type: "datetime" }) + deleted_at: Date | null; + + @Column({ nullable: true, type: "datetime" }) + created_by: string | null; + + @CreateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + created_at: Date; + + @Column({ nullable: true, type: "datetime" }) + updated_by: string | null; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: "datetime", + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; + + @ManyToOne( + () => Account, + (account) => account.userArchive, + { + createForeignKeyConstraints: false, + } + ) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "account_id" }) + account: Account | null; + + @ManyToOne( + () => AccountArchive, + (accountArchive) => accountArchive.userArchive, + { + createForeignKeyConstraints: false, + } + ) // createForeignKeyConstraintsはSQLite用設定値.本番用は別途migrationで設定 + @JoinColumn({ name: "account_id" }) + accountArchive: AccountArchive | null; + + @OneToOne( + () => LicenseArchive, + (licenseArchive) => licenseArchive.userArchive + ) + licenseArchive: LicenseArchive | null; + + @OneToMany( + () => LicenseAllocationHistoryArchive, + (licenseAllocationHistoryArchive) => + licenseAllocationHistoryArchive.userArchive + ) + licenseAllocationHistoryArchive: LicenseAllocationHistoryArchive[] | null; } diff --git a/dictation_function/src/functions/analysisLicenses.ts b/dictation_function/src/functions/analysisLicenses.ts new file mode 100644 index 0000000..22f4709 --- /dev/null +++ b/dictation_function/src/functions/analysisLicenses.ts @@ -0,0 +1,1891 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import { DataSource, Between } from "typeorm"; +import * as dotenv from "dotenv"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { + License, + LicenseAllocationHistory, + LicenseArchive, + LicenseAllocationHistoryArchive, +} from "../entity/license.entity"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { + LICENSE_ALLOCATED_STATUS, + TIERS, + SWITCH_FROM_TYPE, + LICENSE_COUNT_ANALYSIS_CATEGORY_1, + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_ROLE, + LICENSE_COUNT_ANALYSIS_CATEGORY_2, + BLOB_STORAGE_REGION_AU, + USER_ROLES, + BLOB_STORAGE_REGION_US, + BLOB_STORAGE_REGION_EU, + LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_HEADER, + LICENSE_COUNT_ANALYSIS_FRONT_STRING, +} from "../constants"; +import { DateWithDayEndTime } from "../common/types/types"; +import { initializeDataSource } from "../database/initializeDataSource"; +import { bigintTransformer } from "../common/entity"; + +/** + * ライセンス数分析処理のメイン処理:ここから各処理を呼び出す + * @param myTimer + * @param context + */ +export async function analysisLicensesProcessing( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource, + blobstorageService: BlobstorageService +) { + try { + context.log("[IN]analysisLicensesProcessing"); + + const baseData = await getBaseData(context, targetMonthYYYYMM, datasource); + const baseDataFromDeletedAccounts = await getBaseDataFromDeletedAccounts( + context, + targetMonthYYYYMM, + datasource + ); + const outputCsvData = await transferData( + context, + baseData, + baseDataFromDeletedAccounts, + targetMonthYYYYMM + ); + await outputAnalysisLicensesData( + context, + blobstorageService, + outputCsvData + ); + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]analysisLicensesProcessing"); + } +} + +/** + * 集計元のデータをDBから取得する処理 + * @param context + * @param targetMonthYYYYMM + * @param datasource + * 内部関数だがテスト用にexportする + */ +export async function getBaseData( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource +): Promise { + try { + context.log("[IN]getBaseData"); + + // 第五階層のアカウントとユーザーを取得する + // 第五のアカウントを取得 + const accountRepository = datasource.getRepository(Account); + const accountsAndUsersFromTier5 = await accountRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { + user: true, + userArchive: true, + }, + }); + + // 第五階層が保持する有効なライセンスを取得 + const licenseRepository = datasource.getRepository(License); + // 現在時刻を起点とした23:59:59の日付 + const currentDateWithDayEndTime = new DateWithDayEndTime(); + const avairableLicenses = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "(license.expiry_date > :currentDateWithDayEndTime OR license.expiry_date IS NULL)", + { + currentDateWithDayEndTime, + } + ) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に発行したライセンスを取得 + + // timestamp型のDB列をyyyymm形式に変換する場合RDBMS依存の処理が必要になるので、最初の日~最後の日のbetweenで判断する + const year = parseInt(targetMonthYYYYMM.slice(0, 4), 10); + const month = parseInt(targetMonthYYYYMM.slice(4), 10) - 1; // JavaScriptの月は0-indexed + const targetMonthStartDate = new Date(year, month, 1, 0, 0, 0, 0); + const targetMonthEndDate = new Date(year, month + 1, 0, 23, 59, 59, 0); // mysql上ミリ秒999を指定すると四捨五入されて翌日になってしまうのでミリ秒は0を指定 + + const licensesIssuedInTargetMonth = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + created_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に失効したライセンスを取得 + const licensesExpiredInTargetMonth = await licenseRepository + .createQueryBuilder("license") + .innerJoin("license.account", "account") + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + expiry_date: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層がその月におこなったライセンス切り替え情報を取得 + const licenseAllocationHistory = datasource.getRepository( + LicenseAllocationHistory + ); + const switchedlicensesInTargetMonth = await licenseAllocationHistory + .createQueryBuilder("licenseAllocationHistory") + .innerJoin( + "accounts", + "account", + "licenseAllocationHistory.account_id = account.id" + ) + .where("account.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere("licenseAllocationHistory.switch_from_type IN (:...types)", { + types: [SWITCH_FROM_TYPE.CARD, SWITCH_FROM_TYPE.TRIAL], + }) + .andWhere({ + executed_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .getMany(); + + return { + accountsAndUsersFromTier5, + avairableLicenses, + licensesIssuedInTargetMonth, + licensesExpiredInTargetMonth, + switchedlicensesInTargetMonth, + }; + } catch (e) { + context.log("getBaseData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]getBaseData"); + } +} + +/** + * 集計元のデータ(削除されたアカウントの情報)をDBから取得する処理 + * @param context + * @param targetMonthYYYYMM + * @param datasource + * 内部関数だがテスト用にexportする + */ +export async function getBaseDataFromDeletedAccounts( + context: InvocationContext, + targetMonthYYYYMM: string, + datasource: DataSource +): Promise { + try { + context.log("[IN]getBaseDataFromDeletedAccounts"); + + // 第五階層のアカウントとユーザーを取得する + // 第五のアカウントを取得 + const accountArchiveRepository = datasource.getRepository(AccountArchive); + const deletedAccountsAndUsersFromTier5 = + await accountArchiveRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { userArchive: true }, + }); + + // 第五階層が保持する有効なライセンスを取得 + const licenseArchiveRepository = datasource.getRepository(LicenseArchive); + // 現在時刻を起点とした23:59:59の日付 + const currentDateWithDayEndTime = new DateWithDayEndTime(); + const deletedAvairableLicenses = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "(license_archive.expiry_date > :currentDateWithDayEndTime OR license_archive.expiry_date IS NULL)", + { + currentDateWithDayEndTime, + } + ) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に発行したライセンスを取得 + + // timestamp型のDB列をyyyymm形式に変換する場合RDBMS依存の処理が必要になるので、最初の日~最後の日のbetweenで判断する + const year = parseInt(targetMonthYYYYMM.slice(0, 4), 10); + const month = parseInt(targetMonthYYYYMM.slice(4), 10) - 1; // JavaScriptの月は0-indexed + const targetMonthStartDate = new Date(year, month, 1, 0, 0, 0, 0); + const targetMonthEndDate = new Date(year, month + 1, 0, 23, 59, 59, 0); // mysql上ミリ秒999を指定すると四捨五入されて翌日になってしまうのでミリ秒は0を指定 + + const deletedLicensesIssuedInTargetMonth = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + created_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層が保持するその月に失効したライセンスを取得 + const deletedLicensesExpiredInTargetMonth = await licenseArchiveRepository + .createQueryBuilder("license_archive") + .innerJoin("license_archive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere({ + expiry_date: Between(targetMonthStartDate, targetMonthEndDate), + }) + .andWhere("license_archive.status IN (:...statuses)", { + statuses: [ + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ], + }) + .getMany(); + + // 第五階層がその月におこなったライセンス切り替え情報を取得 + const licenseAllocationHistoryArchive = datasource.getRepository( + LicenseAllocationHistoryArchive + ); + const deletedSwitchedlicensesInTargetMonth = + await licenseAllocationHistoryArchive + .createQueryBuilder("licenseAllocationHistory_archive") + .innerJoinAndSelect( + "licenseAllocationHistory_archive.userArchive", + "userArchive" + ) + .innerJoin("userArchive.accountArchive", "accountArchive") + .where("accountArchive.tier = :tier", { tier: TIERS.TIER5 }) + .andWhere( + "licenseAllocationHistory_archive.switch_from_type IN (:...types)", + { + types: [SWITCH_FROM_TYPE.CARD, SWITCH_FROM_TYPE.TRIAL], + } + ) + .andWhere({ + executed_at: Between(targetMonthStartDate, targetMonthEndDate), + }) + .getMany(); + + return { + deletedAccountsAndUsersFromTier5, + deletedAvairableLicenses, + deletedLicensesIssuedInTargetMonth, + deletedLicensesExpiredInTargetMonth, + deletedSwitchedlicensesInTargetMonth, + }; + } catch (e) { + context.log("getBaseDataFromDeletedAccounts failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]getBaseDataFromDeletedAccounts"); + } +} + +/** + * ライセンス数分析処理:Azure Functionの関数として呼び出される処理 + * @param myTimer + * @param context + */ +export async function analysisLicenses( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log("[IN]analysisLicenses"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + try { + const datasource = await initializeDataSource(context); + const blobstorageService = new BlobstorageService(true); + + try { + // 現在の日付より、先月の年月をYYYYMM形式で取得 + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() - 1); + const year = currentDate.getFullYear(); + const month = (currentDate.getMonth() + 1).toString().padStart(2, "0"); // 月は0から始まるため+1する + const formattedDate = `${year}${month}`; + + await analysisLicensesProcessing( + context, + formattedDate, + datasource, + blobstorageService + ); + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } + } finally { + context.log("[OUT]analysisLicenses"); + } +} + +app.timer("analysisLicenses", { + schedule: "0 0 0 1 * *", + handler: analysisLicenses, +}); + +type BaseData = { + // 存在するアカウントの集計元情報 + accountsAndUsersFromTier5: Account[]; + avairableLicenses: License[]; + licensesIssuedInTargetMonth: License[]; + licensesExpiredInTargetMonth: License[]; + switchedlicensesInTargetMonth: LicenseAllocationHistory[]; +}; + +type BaseDataFromDeletedAccounts = { + // 削除されたアカウントの集計元情報 + deletedAccountsAndUsersFromTier5: AccountArchive[]; + deletedAvairableLicenses: LicenseArchive[]; + deletedLicensesIssuedInTargetMonth: LicenseArchive[]; + deletedLicensesExpiredInTargetMonth: LicenseArchive[]; + deletedSwitchedlicensesInTargetMonth: LicenseAllocationHistoryArchive[]; +}; + +type outputDataAnalysisLicensesCSV = { + outputDataUS: string[]; + outputDataEU: string[]; + outputDataAU: string[]; +}; + +/** + * アカウントと紐づくユーザー、ライセンスからCSV出力用の配列を作成する + * @param context + * @param baseData + * @param baseDataFromDeletedAccounts + * @param targetMonthYYYYMM + * @returns outputDataAnalysisLicensesCSV + */ +export async function transferData( + context: InvocationContext, + baseData: BaseData, + baseDataFromDeletedAccounts: BaseDataFromDeletedAccounts, + targetMonthYYYYMM: string +): Promise { + context.log("[IN]transferData"); + const accountsAndUsersFromTier5 = baseData.accountsAndUsersFromTier5; + const validLicenses = baseData.avairableLicenses; + const currentMonthIssuedLicenses = baseData.licensesIssuedInTargetMonth; + const invalidLicenses = baseData.licensesExpiredInTargetMonth; + const switchedLicenses = baseData.switchedlicensesInTargetMonth; + const deletedAccountsAndUsersFromTier5 = + baseDataFromDeletedAccounts.deletedAccountsAndUsersFromTier5; + const deletedValidLicenses = + baseDataFromDeletedAccounts.deletedAvairableLicenses; + const deletedCurrentMonthIssuedLicenses = + baseDataFromDeletedAccounts.deletedLicensesIssuedInTargetMonth; + const deletedInvalidLicenses = + baseDataFromDeletedAccounts.deletedLicensesExpiredInTargetMonth; + const deletedSwitchedLicenses = + baseDataFromDeletedAccounts.deletedSwitchedlicensesInTargetMonth; + + // 出力データ格納配列 + let outputDataUS: string[] = []; + let outputDataEU: string[] = []; + let outputDataAU: string[] = []; + // 出力データのヘッダーを作成 + const header = [ + '"' + LICENSE_COUNT_ANALYSIS_HEADER.ACCOUNT + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.TARGET_YEAE_AND_MONTH + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_1 + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.CATEGORY_2 + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.LICENSE_TYPE + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.ROLE + '",', + '"' + LICENSE_COUNT_ANALYSIS_HEADER.COUNT + '"\r\n', + ] as string[]; + // ヘッダーを出力データに追加 + outputDataUS.push(...header); + outputDataEU.push(...header); + outputDataAU.push(...header); + + // ユーザーIDとロールを格納する配列(型が違う為新たに作成する) + let tier5userIdAndRoles: { id: number; role: string }[] = []; + try { + // 第五階層のアカウントごとにループ + for (const account of accountsAndUsersFromTier5) { + // account.userとaccount.userArchiveが存在しない場合次のアカウントに進む + if (!account.user && !account.userArchive) { + console.log( + "account.user and account.userArchive is not exist.accountId:" + + account.id + ); + continue; + } + + // ユーザーとユーザーアーカイブからユーザーIDとロールを取得する + if (account.user) { + tier5userIdAndRoles = account.user.map((user) => { + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + return { id: bigintTransformer.from(user.id), role: user.role }; + }); + } + if (account.userArchive) { + tier5userIdAndRoles = tier5userIdAndRoles.concat( + account.userArchive.map((userArchive) => { + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + return { + id: bigintTransformer.from(userArchive.id), + role: userArchive.role, + }; + }) + ); + } + // アカウントに紐づくライセンスを取得 + const accountLicenses = validLicenses.filter( + (license) => license.account_id === account.id + ); + // 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const trialLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const normalLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const cardLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated + const usedTrialLicenses = trialLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedNormalLicenses = normalLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedCardLicenses = cardLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 + // (allcated_user_idからユーザーを特定) + // (Author・Typist・None) + const usedTrialLicensesAuthor = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedTrialLicensesTypist = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedTrialLicensesNone = usedTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const usedNormalLicensesAuthor = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedNormalLicensesTypist = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedNormalLicensesNone = usedNormalLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const usedCardLicensesAuthor = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const usedCardLicensesTypist = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const usedCardLicensesNone = usedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + // 使用中のライセンスの数をカウント + const trialLicensesCount = trialLicenses.length; + const normalLicensesCount = normalLicenses.length; + const cardLicensesCount = cardLicenses.length; + const usedTrialLicensesAuthorCount = usedTrialLicensesAuthor.length; + const usedTrialLicensesTypistCount = usedTrialLicensesTypist.length; + const usedTrialLicensesNoneCount = usedTrialLicensesNone.length; + const usedNormalLicensesAuthorCount = usedNormalLicensesAuthor.length; + const usedNormalLicensesTypistCount = usedNormalLicensesTypist.length; + const usedNormalLicensesNoneCount = usedNormalLicensesNone.length; + const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; + const usedCardLicensesTypistCount = usedCardLicensesTypist.length; + const usedCardLicensesNoneCount = usedCardLicensesNone.length; + + // アカウントに紐づく当月発行ライセンスを取得 + const accountCurrentMonthIssuedLicenses = + currentMonthIssuedLicenses.filter( + (license) => license.account_id === account.id + ); + // 当月発行ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const currentMonthIssuedTrialLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const currentMonthIssuedNormalLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const currentMonthIssuedCardLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 当月発行ライセンスの数をカウント + const currentMonthIssuedTrialLicensesCount = + currentMonthIssuedTrialLicenses.length; + const currentMonthIssuedNormalLicensesCount = + currentMonthIssuedNormalLicenses.length; + const currentMonthIssuedCardLicensesCount = + currentMonthIssuedCardLicenses.length; + + // アカウントに紐づく失効ライセンスを取得 + const accountInvalidLicenses = invalidLicenses.filter( + (license) => license.account_id === account.id + ); + // 失効ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const invalidTrialLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const invalidNormalLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const invalidCardLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // どのロールのユーザーに割り当てたまま失効したライセンスかを判別し、ロールごとに分ける。 + //(allcated_user_idからユーザーを特定、値がない場合は未割当) + // (Author・Typist・None・未割当) + const invalidTrialLicensesAuthor = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidTrialLicensesTypist = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidTrialLicensesNone = invalidTrialLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidNormalLicensesAuthor = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidNormalLicensesTypist = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidNormalLicensesNone = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidCardLicensesAuthor = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.AUTHOR + ) + ); + const invalidCardLicensesTypist = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.TYPIST + ) + ); + const invalidCardLicensesNone = invalidCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + user.id === license.allocated_user_id && + user.role === USER_ROLES.NONE + ) + ); + const invalidTrialLicensesUnallocated = invalidTrialLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidNormalLicensesUnallocated = invalidNormalLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidCardLicensesUnallocated = invalidCardLicenses.filter( + (license) => !license.allocated_user_id + ); + // 失効ライセンスの数をカウント + const invalidTrialLicensesAuthorCount = invalidTrialLicensesAuthor.length; + const invalidTrialLicensesTypistCount = invalidTrialLicensesTypist.length; + const invalidTrialLicensesNoneCount = invalidTrialLicensesNone.length; + const invalidTrialLicensesUnallocatedCount = + invalidTrialLicensesUnallocated.length; + const invalidNormalLicensesAuthorCount = + invalidNormalLicensesAuthor.length; + const invalidNormalLicensesTypistCount = + invalidNormalLicensesTypist.length; + const invalidNormalLicensesNoneCount = invalidNormalLicensesNone.length; + const invalidNormalLicensesUnallocatedCount = + invalidNormalLicensesUnallocated.length; + const invalidCardLicensesAuthorCount = invalidCardLicensesAuthor.length; + const invalidCardLicensesTypistCount = invalidCardLicensesTypist.length; + const invalidCardLicensesNoneCount = invalidCardLicensesNone.length; + const invalidCardLicensesUnallocatedCount = + invalidCardLicensesUnallocated.length; + + // アカウントに紐づく切り替えライセンスを取得 + const accountSwitchedLicenses = switchedLicenses.filter( + (license) => license.account_id === account.id + ); + // どの種別のライセンスから切り替えられたかで分ける(switch_from_typeカラムで判別)(トライアル・カード) + const switchedTrialLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.TRIAL + ); + const switchedCardLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.CARD + ); + // どのロールのユーザーに対して切り替えが行われたかで分ける。 + //(user_idからユーザーを特定) + //(Typist・Author・None) + const switchedTypistLicensesTypist = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.TYPIST + ) + ); + const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.AUTHOR + ) + ); + const switchedTypistLicensesNone = switchedTrialLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.NONE + ) + ); + const switchedCardLicensesTypist = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.TYPIST + ) + ); + const switchedCardLicensesAuthor = switchedCardLicenses.filter( + (license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.AUTHOR + ) + ); + const switchedCardLicensesNone = switchedCardLicenses.filter((license) => + tier5userIdAndRoles.find( + (user) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + user.id === bigintTransformer.from(license.user_id) && + user.role === USER_ROLES.NONE + ) + ); + // 切り替えライセンスの数をカウント + const switchedTypistLicensesTypistCount = + switchedTypistLicensesTypist.length; + const switchedTypistLicensesAuthorCount = + switchedTypistLicensesAuthor.length; + const switchedTypistLicensesNoneCount = switchedTypistLicensesNone.length; + const switchedCardLicensesTypistCount = switchedCardLicensesTypist.length; + const switchedCardLicensesAuthorCount = switchedCardLicensesAuthor.length; + const switchedCardLicensesNoneCount = switchedCardLicensesNone.length; + + // 国に対応したリージョンに応じた配列に格納する + if (BLOB_STORAGE_REGION_US.includes(account.country)) { + outputDataUS = outputDataUS.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_EU.includes(account.country)) { + outputDataEU = outputDataEU.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_AU.includes(account.country)) { + outputDataAU = outputDataAU.concat( + await createOutputData( + context, + account.company_name, + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else { + throw new Error("invalid country"); + } + } + // 削除版 + tier5userIdAndRoles = []; + // 第五階層のアカウントごとにループ + for (const account of deletedAccountsAndUsersFromTier5) { + // account.userArchiveが存在しない場合次のアカウントに進む + if (!account.userArchive) { + continue; + } + // アカウントに紐づくユーザーを取得 + if (account.userArchive) { + tier5userIdAndRoles = account.userArchive.map((userArchive) => { + return { + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + id: bigintTransformer.from(userArchive.id), + role: userArchive.role, + }; + }); + } + // アカウントに紐づくライセンスを取得 + const accountLicenses = deletedValidLicenses.filter( + (license) => license.account_id === account.id + ); + // 抽出したライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const trialLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const normalLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const cardLicenses = accountLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 種別ごとのライセンスから使用中のライセンスを抽出statusカラムがAllocated + const usedTrialLicenses = trialLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedNormalLicenses = normalLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + const usedCardLicenses = cardLicenses.filter( + (license) => license.status === LICENSE_ALLOCATED_STATUS.ALLOCATED + ); + // どのロールのユーザーが使用しているライセンスかを判別し、ロールごとに分ける。 + // (allcated_user_idからユーザーを特定) + // (Author・Typist・None) + const usedTrialLicensesAuthor = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedTrialLicensesTypist = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedTrialLicensesNone = usedTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const usedNormalLicensesAuthor = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedNormalLicensesTypist = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedNormalLicensesNone = usedNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const usedCardLicensesAuthor = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const usedCardLicensesTypist = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const usedCardLicensesNone = usedCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + // 使用中のライセンスの数をカウント + const trialLicensesCount = trialLicenses.length; + const normalLicensesCount = normalLicenses.length; + const cardLicensesCount = cardLicenses.length; + const usedTrialLicensesAuthorCount = usedTrialLicensesAuthor.length; + const usedTrialLicensesTypistCount = usedTrialLicensesTypist.length; + const usedTrialLicensesNoneCount = usedTrialLicensesNone.length; + const usedNormalLicensesAuthorCount = usedNormalLicensesAuthor.length; + const usedNormalLicensesTypistCount = usedNormalLicensesTypist.length; + const usedNormalLicensesNoneCount = usedNormalLicensesNone.length; + const usedCardLicensesAuthorCount = usedCardLicensesAuthor.length; + const usedCardLicensesTypistCount = usedCardLicensesTypist.length; + const usedCardLicensesNoneCount = usedCardLicensesNone.length; + + // アカウントに紐づく当月発行ライセンスを取得 + const accountCurrentMonthIssuedLicenses = + deletedCurrentMonthIssuedLicenses.filter( + (license) => license.account_id === account.id + ); + // 当月発行ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const currentMonthIssuedTrialLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const currentMonthIssuedNormalLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const currentMonthIssuedCardLicenses = + accountCurrentMonthIssuedLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // 当月発行ライセンスの数をカウント + const currentMonthIssuedTrialLicensesCount = + currentMonthIssuedTrialLicenses.length; + const currentMonthIssuedNormalLicensesCount = + currentMonthIssuedNormalLicenses.length; + const currentMonthIssuedCardLicensesCount = + currentMonthIssuedCardLicenses.length; + + // アカウントに紐づく失効ライセンスを取得 + const accountInvalidLicenses = deletedInvalidLicenses.filter( + (license) => license.account_id === account.id + ); + // 失効ライセンスを種別ごとに分ける。(typeカラムで判別)(トライアル・通常・カード) + const invalidTrialLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.TRIAL + ); + const invalidNormalLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.NORMAL + ); + const invalidCardLicenses = accountInvalidLicenses.filter( + (license) => license.type === LICENSE_TYPE.CARD + ); + // どのロールのユーザーに割り当てたまま失効したライセンスかを判別し、ロールごとに分ける。 + //(allcated_user_idからユーザーを特定、値がない場合は未割当) + // (Author・Typist・None・未割当) + const invalidTrialLicensesAuthor = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidTrialLicensesTypist = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidTrialLicensesNone = invalidTrialLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidNormalLicensesAuthor = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidNormalLicensesTypist = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidNormalLicensesNone = invalidNormalLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidCardLicensesAuthor = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.AUTHOR + ); + const invalidCardLicensesTypist = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.TYPIST + ); + const invalidCardLicensesNone = invalidCardLicenses.filter( + (license) => + tier5userIdAndRoles?.find( + (user) => user.id === license.allocated_user_id + )?.role === USER_ROLES.NONE + ); + const invalidTrialLicensesUnallocated = invalidTrialLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidNormalLicensesUnallocated = invalidNormalLicenses.filter( + (license) => !license.allocated_user_id + ); + const invalidCardLicensesUnallocated = invalidCardLicenses.filter( + (license) => !license.allocated_user_id + ); + // 失効ライセンスの数をカウント + const invalidTrialLicensesAuthorCount = invalidTrialLicensesAuthor.length; + const invalidTrialLicensesTypistCount = invalidTrialLicensesTypist.length; + const invalidTrialLicensesNoneCount = invalidTrialLicensesNone.length; + const invalidTrialLicensesUnallocatedCount = + invalidTrialLicensesUnallocated.length; + const invalidNormalLicensesAuthorCount = + invalidNormalLicensesAuthor.length; + const invalidNormalLicensesTypistCount = + invalidNormalLicensesTypist.length; + const invalidNormalLicensesNoneCount = invalidNormalLicensesNone.length; + const invalidNormalLicensesUnallocatedCount = + invalidNormalLicensesUnallocated.length; + const invalidCardLicensesAuthorCount = invalidCardLicensesAuthor.length; + const invalidCardLicensesTypistCount = invalidCardLicensesTypist.length; + const invalidCardLicensesNoneCount = invalidCardLicensesNone.length; + const invalidCardLicensesUnallocatedCount = + invalidCardLicensesUnallocated.length; + + // アカウントに紐づく切り替えライセンスを取得 + const accountSwitchedLicenses = deletedSwitchedLicenses.filter( + (license) => license.account_id === account.id + ); + // どの種別のライセンスから切り替えられたかで分ける(switch_from_typeカラムで判別)(トライアル・カード) + const switchedTrialLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.TRIAL + ); + const switchedCardLicenses = accountSwitchedLicenses.filter( + (license) => license.switch_from_type === SWITCH_FROM_TYPE.CARD + ); + // どのロールのユーザーに対して切り替えが行われたかで分ける。 + //(user_idからユーザーを特定) + //(Typist・Author・None) + const switchedTypistLicensesTypist = switchedTrialLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.TYPIST + ); + const switchedTypistLicensesAuthor = switchedTrialLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.AUTHOR + ); + const switchedTypistLicensesNone = switchedTrialLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.NONE + ); + const switchedCardLicensesTypist = switchedCardLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.TYPIST + ); + const switchedCardLicensesAuthor = switchedCardLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.AUTHOR + ); + const switchedCardLicensesNone = switchedCardLicenses.filter( + (license) => + // XXX entityを修正すべきだが、時期的に影響範囲が大きいため、ここで変換する + // 本対応は#3928で行う + tier5userIdAndRoles?.find( + (user) => user.id === bigintTransformer.from(license.user_id) + )?.role === USER_ROLES.NONE + ); + // 切り替えライセンスの数をカウント + const switchedTypistLicensesTypistCount = + switchedTypistLicensesTypist.length; + const switchedTypistLicensesAuthorCount = + switchedTypistLicensesAuthor.length; + const switchedTypistLicensesNoneCount = switchedTypistLicensesNone.length; + const switchedCardLicensesTypistCount = switchedCardLicensesTypist.length; + const switchedCardLicensesAuthorCount = switchedCardLicensesAuthor.length; + const switchedCardLicensesNoneCount = switchedCardLicensesNone.length; + + // 国に対応したリージョンに応じた配列に格納する + if (BLOB_STORAGE_REGION_US.includes(account.country)) { + outputDataUS = outputDataUS.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_EU.includes(account.country)) { + outputDataEU = outputDataEU.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } else if (BLOB_STORAGE_REGION_AU.includes(account.country)) { + outputDataAU = outputDataAU.concat( + await createOutputData( + context, + account.id.toString(), + targetMonthYYYYMM, + trialLicensesCount, + normalLicensesCount, + cardLicensesCount, + usedTrialLicensesAuthorCount, + usedTrialLicensesTypistCount, + usedTrialLicensesNoneCount, + usedNormalLicensesAuthorCount, + usedNormalLicensesTypistCount, + usedNormalLicensesNoneCount, + usedCardLicensesAuthorCount, + usedCardLicensesTypistCount, + usedCardLicensesNoneCount, + currentMonthIssuedTrialLicensesCount, + currentMonthIssuedNormalLicensesCount, + currentMonthIssuedCardLicensesCount, + invalidTrialLicensesAuthorCount, + invalidTrialLicensesTypistCount, + invalidTrialLicensesNoneCount, + invalidTrialLicensesUnallocatedCount, + invalidNormalLicensesAuthorCount, + invalidNormalLicensesTypistCount, + invalidNormalLicensesNoneCount, + invalidNormalLicensesUnallocatedCount, + invalidCardLicensesAuthorCount, + invalidCardLicensesTypistCount, + invalidCardLicensesNoneCount, + invalidCardLicensesUnallocatedCount, + switchedTypistLicensesAuthorCount, + switchedTypistLicensesTypistCount, + switchedTypistLicensesNoneCount, + switchedCardLicensesAuthorCount, + switchedCardLicensesTypistCount, + switchedCardLicensesNoneCount + ) + ); + } + } + return { + outputDataUS: outputDataUS, + outputDataEU: outputDataEU, + outputDataAU: outputDataAU, + }; + } catch (e) { + context.log("transferData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]transferData"); + } +} + +/** + * 出力データ作成 + * @param context + * @param company_name + * @param targetMonthYYYYMM + * @param trialLicensesCount + * @param normalLicensesCount + * @param cardLicensesCount + * @param usedTrialLicensesAuthorCount + * @param usedTrialLicensesTypistCount + * @param usedTrialLicensesNoneCount + * @param usedNormalLicensesAuthorCount + * @param usedNormalLicensesTypistCount + * @param usedNormalLicensesNoneCount + * @param usedCardLicensesAuthorCount + * @param usedCardLicensesTypistCount + * @param usedCardLicensesNoneCount + * @param currentMonthIssuedTrialLicensesCount + * @param currentMonthIssuedNormalLicensesCount + * @param currentMonthIssuedCardLicensesCount + * @param invalidTrialLicensesAuthorCount + * @param invalidTrialLicensesTypistCount + * @param invalidTrialLicensesNoneCount + * @param invalidTrialLicensesUnallocatedCount + * @param invalidNormalLicensesAuthorCount + * @param invalidNormalLicensesTypistCount + * @param invalidNormalLicensesNoneCount + * @param invalidNormalLicensesUnallocatedCount + * @param invalidCardLicensesAuthorCount + * @param invalidCardLicensesTypistCount + * @param invalidCardLicensesNoneCount + * @param invalidCardLicensesUnallocatedCount + * @param switchedTypistLicensesAuthorCount + * @param switchedTypistLicensesTypistCount + * @param switchedTypistLicensesNoneCount + * @param switchedCardLicensesAuthorCount + * @param switchedCardLicensesTypistCount + * @param switchedCardLicensesNoneCount + * @returns string[] + */ +async function createOutputData( + context: InvocationContext, + company_name: string, + targetMonthYYYYMM: string, + trialLicensesCount: number, + normalLicensesCount: number, + cardLicensesCount: number, + usedTrialLicensesAuthorCount: number, + usedTrialLicensesTypistCount: number, + usedTrialLicensesNoneCount: number, + usedNormalLicensesAuthorCount: number, + usedNormalLicensesTypistCount: number, + usedNormalLicensesNoneCount: number, + usedCardLicensesAuthorCount: number, + usedCardLicensesTypistCount: number, + usedCardLicensesNoneCount: number, + currentMonthIssuedTrialLicensesCount: number, + currentMonthIssuedNormalLicensesCount: number, + currentMonthIssuedCardLicensesCount: number, + invalidTrialLicensesAuthorCount: number, + invalidTrialLicensesTypistCount: number, + invalidTrialLicensesNoneCount: number, + invalidTrialLicensesUnallocatedCount: number, + invalidNormalLicensesAuthorCount: number, + invalidNormalLicensesTypistCount: number, + invalidNormalLicensesNoneCount: number, + invalidNormalLicensesUnallocatedCount: number, + invalidCardLicensesAuthorCount: number, + invalidCardLicensesTypistCount: number, + invalidCardLicensesNoneCount: number, + invalidCardLicensesUnallocatedCount: number, + switchedTypistLicensesAuthorCount: number, + switchedTypistLicensesTypistCount: number, + switchedTypistLicensesNoneCount: number, + switchedCardLicensesAuthorCount: number, + switchedCardLicensesTypistCount: number, + switchedCardLicensesNoneCount: number +): Promise { + context.log("[IN]createOutputData"); + const resultOutputData: string[] = []; + try { + // アカウントが保持するトライアルライセンス[] + resultOutputData.push( + // 会社名(ダブルクォーテーションで囲む) + '"' + company_name + '",', + // 対象年月(先月)YYYYMM + // 2024年3月に実行した場合:202402 + '"' + targetMonthYYYYMM + '",', + // カテゴリー1 + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + // カテゴリー2 + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + // ライセンス種別 + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + // 役割 + '"' + "" + '",', + // 数量 + '"' + trialLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + "" + '",', + '"' + normalLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持するカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.OWNER_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + "" + '",', + '"' + cardLicensesCount.toString() + '"\r\n' + ); + // Authorが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedTrialLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedTrialLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中のトライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedTrialLicensesNoneCount.toString() + '"\r\n' + ); + // Authorが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedNormalLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedNormalLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中の通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedNormalLicensesNoneCount.toString() + '"\r\n' + ); + // Authorが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + usedCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + usedCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneが使用中のカードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_2.IN_USE_LICENSES + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + usedCardLicensesNoneCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + "" + '",', + '"' + currentMonthIssuedTrialLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + "" + '",', + '"' + currentMonthIssuedNormalLicensesCount.toString() + '"\r\n' + ); + // アカウントが保持する当月発行カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.NEW_ISSUE_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + "" + '",', + '"' + currentMonthIssuedCardLicensesCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidTrialLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidTrialLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidTrialLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効トライアルライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidTrialLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidNormalLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidNormalLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidNormalLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効通常ライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.STANDARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidNormalLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + invalidCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + invalidCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneに割り当てられたままの失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + invalidCardLicensesNoneCount.toString() + '"\r\n' + ); + // 未割当の失効カードライセンス[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.INVALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.UNALLOCATED + '",', + '"' + invalidCardLicensesUnallocatedCount.toString() + '"\r\n' + ); + // Authorにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + switchedTypistLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + switchedTypistLicensesTypistCount.toString() + '"\r\n' + ); + // Noneにトライアルライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_TRIAL + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + switchedTypistLicensesNoneCount.toString() + '"\r\n' + ); + // Authorにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.AUTHOR + '",', + '"' + switchedCardLicensesAuthorCount.toString() + '"\r\n' + ); + // Typistにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.TYPIST + '",', + '"' + switchedCardLicensesTypistCount.toString() + '"\r\n' + ); + // Noneにカードライセンスからの切り替え[] + resultOutputData.push( + '"' + company_name + '",', + '"' + targetMonthYYYYMM + '",', + '"' + LICENSE_COUNT_ANALYSIS_CATEGORY_1.VALID_LICENSES + '",', + '"' + "" + '",', + '"' + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE.SWITCH_FROM_CARD + '",', + '"' + LICENSE_COUNT_ANALYSIS_ROLE.NONE + '",', + '"' + switchedCardLicensesNoneCount.toString() + '"\r\n' + ); + return resultOutputData; + } catch (e) { + context.log("createOutputData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]createOutputData"); + } +} + +/** + * 出力データを第一階層のストレージアカウントにCSVファイルとして出力する + * @param context + * @param blobstorageService: BlobstorageService, + * @param outputCsvData: outputDataAnalysisLicensesCSV + */ +export async function outputAnalysisLicensesData( + context: InvocationContext, + blobstorageService: BlobstorageService, + outputCsvData: outputDataAnalysisLicensesCSV +): Promise { + context.log("[IN]outputAnalysisLicensesData"); + try { + let csvContentUS = ""; + let csvContentEU = ""; + let csvContentAU = ""; + // 出力日時を取得 + const outputDateTime = new Date().toISOString(); + // YYYYMMDDHH24MISS形式に変換 + const outputDateTimeYYYYMMDDHH24MISS = outputDateTime.replace(/[-T:]/g, ""); + // 出力ファイル名を作成 + const outputFileNameUS = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_US_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + const outputFileNameEU = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_EU_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + const outputFileNameAU = + LICENSE_COUNT_ANALYSIS_FRONT_STRING + + `_AU_${outputDateTimeYYYYMMDDHH24MISS}.csv`; + + // outputDataUSの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataUS.length; i++) { + //カンマ区切りの文字列を作成 + csvContentUS += outputCsvData.outputDataUS[i]; + } + // outputDataEUの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataEU.length; i++) { + //カンマ区切りの文字列を作成 + csvContentEU += outputCsvData.outputDataEU[i]; + } + // outputDataAUの配列をCSV形式に変換 + for (let i = 0; i < outputCsvData.outputDataAU.length; i++) { + //カンマ区切りの文字列を作成 + csvContentAU += outputCsvData.outputDataAU[i]; + } + await blobstorageService.createContainerAnalysis(context); + // 出力ファイル名を指定して出力 + const resultUS = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameUS, + csvContentUS + ); + const resultEU = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameEU, + csvContentEU + ); + const resultAU = await blobstorageService.uploadFileAnalysisLicensesCSV( + context, + outputFileNameAU, + csvContentAU + ); + // 出力結果を返却 + // 3つのリージョンの出力が全て成功した場合にtrueを返却 + return resultUS && resultEU && resultAU; + } catch (e) { + context.log("outputAnalysisLicensesData failed."); + context.error(e); + throw e; + } finally { + context.log("[OUT]outputAnalysisLicensesData"); + } +} diff --git a/dictation_function/src/functions/analysisLicensesManualRetry.ts b/dictation_function/src/functions/analysisLicensesManualRetry.ts new file mode 100644 index 0000000..8b9dfaf --- /dev/null +++ b/dictation_function/src/functions/analysisLicensesManualRetry.ts @@ -0,0 +1,76 @@ +import { + HttpRequest, + HttpResponseInit, + InvocationContext, + app, + HttpMethod, +} from "@azure/functions"; +import { analysisLicensesProcessing } from "./analysisLicenses"; +import * as dotenv from "dotenv"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { initializeDataSource } from "../database/initializeDataSource"; +import { HTTP_METHODS, HTTP_STATUS_CODES } from "../constants"; + +export async function analysisLicensesManualRetry( + req: HttpRequest, + context: InvocationContext +): Promise { + context.log(req); + try { + if (req.method === HTTP_METHODS.POST) { + context.log("[IN]analysisLicensesManualRetry"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + const datasource = await initializeDataSource(context); + const blobstorageService = new BlobstorageService(true); + + try { + // 現在の日付より、先月の年月をYYYYMM形式で取得 + const currentDate = new Date(); + currentDate.setMonth(currentDate.getMonth() - 1); + const year = currentDate.getFullYear(); + const month = (currentDate.getMonth() + 1).toString().padStart(2, "0"); // 月は0から始まるため+1する + const formattedDate = `${year}${month}`; + + await analysisLicensesProcessing( + context, + formattedDate, + datasource, + blobstorageService + ); + + return { + status: HTTP_STATUS_CODES.OK, + body: "analysisLicensesProcessing has been triggered.", + }; + } catch (e) { + context.log("analysisLicensesProcessing failed."); + context.error(e); + throw e; + } + } else { + context.log(`Please use the POST method. method = [${req.method}]`); + return { + status: HTTP_STATUS_CODES.BAD_REQUEST, + body: `Please use the POST method. method = [${req.method}]`, + }; + } + } catch (e) { + context.log("analysisLicensesManualRetry failed."); + context.error(e); + return { + status: HTTP_STATUS_CODES.INTERNAL_SERVER_ERROR, + body: "analysisLicensesManualRetry failed.", + }; + } finally { + context.log("[OUT]analysisLicensesManualRetry"); + } +} + +// httpトリガは定時処理licenseAutoAllocationの異常時の手動再実行用 +app.http("analysisLicensesManualRetry", { + methods: [HTTP_METHODS.POST as HttpMethod], + authLevel: "function", + handler: analysisLicensesManualRetry, +}); diff --git a/dictation_function/src/functions/deleteAudioFiles.ts b/dictation_function/src/functions/deleteAudioFiles.ts new file mode 100644 index 0000000..a8382ae --- /dev/null +++ b/dictation_function/src/functions/deleteAudioFiles.ts @@ -0,0 +1,187 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import { DataSource, In } from "typeorm"; +import { Task } from "../entity/task.entity"; +import { AudioFile } from "../entity/audio_file.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; +import { MANUAL_RECOVERY_REQUIRED } from "../constants"; +import * as dotenv from "dotenv"; +import { initializeDataSource } from "../database/initializeDataSource"; +import { AudioBlobStorageService } from "../blobstorage/audioBlobStorage.service"; + +app.timer("deleteAudioFiles", { + schedule: "0 3 * * *", // 毎日UTC 3:00に実行 + handler: deleteAudioFiles, +}); + +// 削除対象となる音声ファイルとタスクを特定する為の情報 +type TargetTaskInfo = { + /** + * タスクID + * column: tasks.id + */ + id: number; + + /** + * ファイルID + * column: audio_files.id + */ + audio_file_id: number; + + /** + * ファイル名 + * column: audio_files.raw_file_name + */ + raw_file_name: string; + + /** + * アカウントID + * column: accounts.id + */ + account_id: number; + + /** + * アカウントの所属する地域 + * column: accounts.country + */ + country: string; +}; + +export async function deleteAudioFilesProcessing( + context: InvocationContext, + dataSource: DataSource, + blobStorageService: AudioBlobStorageService, + now: Date +): Promise { + context.log(`[IN] deleteAudioFilesProcessing. now=${now}`); + try { + // 削除対象のタスクとファイルを取得 + const targets = await getProcessTargets(dataSource, now); + context.log(`delete targets: ${JSON.stringify(targets)}`); + + // DBからレコードを削除 + await deleteRecords(dataSource, targets); + + // タスクに紐づくファイルを削除 + for (const target of targets) { + try { + const { account_id, country, raw_file_name } = target; + await blobStorageService.deleteFile( + context, + account_id, + country, + raw_file_name + ); + context.log(`file delete success. target=${JSON.stringify(target)}`); + } catch (e) { + context.log( + `${MANUAL_RECOVERY_REQUIRED} file delete failed. target=${JSON.stringify( + target + )}` + ); + } + } + } catch (error) { + // DB関連で例外が発生した場合、アラートを出す為のログを出力する + context.error( + `${MANUAL_RECOVERY_REQUIRED} Failed to execute auto file deletion function. error=${error}` + ); + throw error; + } finally { + context.log(`[OUT] deleteAudioFilesProcessing`); + } +} + +async function deleteAudioFiles( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log("[IN]deleteAudioFiles"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + const dataSource = await initializeDataSource(context); + try { + await deleteAudioFilesProcessing( + context, + dataSource, + new AudioBlobStorageService(), + new Date() + ); + } catch (e) { + context.log("deleteAudioFilesProcessing failed."); + context.error(e); + throw e; + } finally { + await dataSource.destroy(); + context.log("[OUT]deleteAudioFiles"); + } +} + +/** + * タスクに紐づくファイルを削除する + * @param dataSource + * @param targets + * @returns Promise + */ +// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13 +export async function deleteRecords( + dataSource: DataSource, + targets: TargetTaskInfo[] +): Promise { + const taskIds = targets.map((target) => target.id); + const audioFileIds = targets.map((target) => target.audio_file_id); + + // taskIdsに紐づくタスクを削除 + await dataSource.transaction(async (manager) => { + const taskRepository = manager.getRepository(Task); + const audioFileRepository = manager.getRepository(AudioFile); + const audioOptionItem = manager.getRepository(AudioOptionItem); + + // tasksテーブルから削除 + await taskRepository.delete({ + id: In(taskIds), + }); + // audio_filesテーブルから削除 + await audioFileRepository.delete({ + id: In(audioFileIds), + }); + // audio_option_itemsテーブルから削除 + await audioOptionItem.delete({ + audio_file_id: In(audioFileIds), + }); + }); +} + +/* + * ファイル削除対象のタスクを取得する + * @param dataSource + * @param now + * @returns Promise<{ account_id: number; audio_file_id: number; file_name: string; }[]> + */ +// テスト容易性を高めて開発効率を上げるため、本来はexport不要だがexportを行う 2024/03/13 +export async function getProcessTargets( + dataSource: DataSource, + now: Date +): Promise { + return await dataSource + .createQueryBuilder(Task, "tasks") + // SELECTでエイリアスを指定して、結果オブジェクトのプロパティとTargetTaskInfoのプロパティを一致させる + .select("tasks.id", "id") + .addSelect("tasks.audio_file_id", "audio_file_id") + .addSelect("audio_files.raw_file_name", "raw_file_name") + .addSelect("accounts.id", "account_id") + .addSelect("accounts.country", "country") + .where("tasks.finished_at IS NOT NULL") + .innerJoin("accounts", "accounts", "accounts.id = tasks.account_id") + .innerJoin( + "audio_files", + "audio_files", + "audio_files.id = tasks.audio_file_id" + ) + .andWhere("accounts.auto_file_delete = true") + .andWhere( + "DATE_ADD(tasks.finished_at, INTERVAL accounts.file_retention_days DAY) < :now", + { now } + ) + .getRawMany(); +} diff --git a/dictation_function/src/functions/importUsers.ts b/dictation_function/src/functions/importUsers.ts new file mode 100644 index 0000000..0a34802 --- /dev/null +++ b/dictation_function/src/functions/importUsers.ts @@ -0,0 +1,531 @@ +import { app, InvocationContext, Timer } from "@azure/functions"; +import * as dotenv from "dotenv"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { + ADMIN_ROLES, + IMPORT_USERS_MAX_DURATION_MINUTES, + IMPORT_USERS_STAGE_FILE_NAME, + IMPORT_USERS_STAGES, + RoleNumberMap, + ROW_START_INDEX, + SYSTEM_IMPORT_USERS, + TIERS, +} from "../constants"; +import { ErrorRow, ImportData } from "../blobstorage/types/types"; +import { Configuration, UsersApi } from "../api"; +import { createErrorObject } from "../common/errors/utils"; +import { sign, getJwtKey } from "../common/jwt"; +import { AccessToken, SystemAccessToken } from "../common/jwt/types"; +import { isImportJson, isStageJson } from "../blobstorage/types/guards"; +import https from "https"; +import globalAxios from "axios"; + +// すべてのリクエストのヘッダーにX-Requested-Withを追加 +globalAxios.interceptors.request.use((config) => { + // headersがあれば追加、なければ新規作成 + config.headers = config.headers || {}; + // X-Requested-Withを追加 + config.headers["X-Requested-With"] = "XMLHttpRequest"; + return config; +}); + +export async function importUsersProcessing( + context: InvocationContext, + blobstorageService: BlobstorageService, + userApi: UsersApi +): Promise { + context.log(`[IN] importUsersProcessing`); + try { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + + const startUnixTime = getCurrentUnixTime(); + context.log(`importUsersProcessing start: ${startUnixTime}`); + + // ファイルが存在する間ループ + while (true) { + // Blobストレージからファイル名の一覧を取得(stage.json以外) + const bloblist = await blobstorageService.listBlobs(context); + context.log(bloblist); + + // stage.json以外のファイルが存在しない場合は処理中断 + if (bloblist.length === 0) { + break; + } + + // ファイルのうち、日付が最も古いファイルを取得 + let targetFileName = bloblist.sort().at(0); + if (targetFileName === undefined) { + throw new Error("targetFileName is undefined"); + } + let row = ROW_START_INDEX; + + // stage.jsonを取得(ダウンロード)して読み込む + let stageData = await blobstorageService.downloadFileData( + context, + IMPORT_USERS_STAGE_FILE_NAME + ); + + // stage.jsonが存在しない場合は、新規作成する + if (stageData === undefined) { + stageData = JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.CREATED, + }); + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + stageData + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.CREATED} filename: ${targetFileName}` + ); + } + } + + const stage = JSON.parse(stageData); + + if (!isStageJson(stage)) { + throw new Error("stage.json is invalid"); + } + context.log(`start stage: ${JSON.stringify(stage)}`); + + // 作業中のstage.jsonが存在する場合は、処理を再開する + if ( + stage.state !== IMPORT_USERS_STAGES.CREATED && + stage.state !== IMPORT_USERS_STAGES.DONE + ) { + context.log( + `stage is pending. filename: ${stage.filename} row: ${stage.row}` + ); + + // stage.jsonが存在し、内部状態が処理中で、最終更新日時が10分以上前だった場合は処理中断とみなして途中から再開 + const nowUnixTime = getCurrentUnixTime(); + if (nowUnixTime - stage.update > 10 * 60) { + // stage.jsonの内容から処理対象のfilepathを特定する + context.log(`pending filename: ${stage.filename}`); + if (stage.filename === undefined) { + context.log("stage.filename is undefined"); + break; + } + targetFileName = stage.filename; + // 処理開始行をstage.jsonを元に復元する + row = stage.row ?? ROW_START_INDEX; + } else { + // 内部状態が処理中であれば処理中断(処理が終わる前にTimerから再度起動されてしまったケース) + context.log("stage is processing"); + break; + } + } + + { + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.PRAPARE, + filename: targetFileName, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.PRAPARE} filename: ${targetFileName}` + ); + } + } + + // 対象ファイルをダウンロードして読み込む + const importsData = await blobstorageService.downloadFileData( + context, + targetFileName + ); + + // 一括登録ユーザー一覧をメモリ上に展開 + const imports = + importsData === undefined ? undefined : JSON.parse(importsData); + if (!isImportJson(context, imports)) { + throw new Error(`json: ${targetFileName} is invalid`); + } + + if (imports === undefined) { + break; + } + + // 代行操作トークンを発行する + const accsessToken = await generateDelegationAccessToken( + context, + imports.external_id, + imports.user_role + ); + + const errors: ErrorRow[] = []; + // 一括登録ユーザー一覧をループして、一括登録ユーザーを一括登録する(中断された場合は途中から再開するため、sliceで開始行を指定する) + for (const user of imports.data.slice(row - ROW_START_INDEX)) { + { + // stage.jsonを更新(ユーザー追加開始) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.START, + filename: targetFileName, + row: row, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.START} filename: ${targetFileName} row: ${row}` + ); + } + } + + try { + if (!checkUser(context, user, targetFileName, row)) { + throw new Error( + `Invalid user data. filename: ${targetFileName} row: ${row}` + ); + } + + // ユーザーを追加する + await addUser(context, userApi, user, accsessToken); + } catch (e) { + const error = createErrorObject(e); + context.log(error); + // エラーが発生したらエラーコードを控えておく + errors.push({ row: row, error: error.code, name: user.name }); + } + + { + // stage.jsonを更新(ユーザー追加完了) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.COMPLETE, + filename: targetFileName, + row: row, + errors: errors, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.COMPLETE} filename: ${targetFileName} row: ${row}` + ); + } + } + row++; + + // 500ms待機 + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + // 処理対象のユーザー一覧ファイルを削除する + await blobstorageService.deleteFile(context, targetFileName); + + // システムトークンを発行 + const systemToken = await generateSystemToken(context); + + // 一括登録完了メールを送信する(ODMS Cloudの一括追加完了APIを呼び出す) + await userApi.multipleImportsComplate( + { + accountId: imports.account_id, + filename: imports.file_name, + requestTime: getCurrentUnixTime(), + errors: errors.map((error) => { + return { + name: error.name, + line: error.row, + errorCode: error.error, + }; + }), + }, + { + headers: { authorization: `Bearer ${systemToken}` }, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + } + ); + + { + // stage.jsonを更新(処理完了) + const updateSuccess = await blobstorageService.updateFile( + context, + IMPORT_USERS_STAGE_FILE_NAME, + JSON.stringify({ + update: getCurrentUnixTime(), + state: IMPORT_USERS_STAGES.DONE, + }) + ); + if (!updateSuccess) { + throw new Error( + `update stage.json failed. state: ${IMPORT_USERS_STAGES.DONE} filename: ${targetFileName}` + ); + } + } + + // 経過時間を確認して、30分以上経過していたら処理を中断する + { + const currentUnixTime = getCurrentUnixTime(); + // 時間の差分を計算(秒) + const elapsedSec = currentUnixTime - startUnixTime; + + // 30分以上経過していたら処理を中断する + if (elapsedSec > IMPORT_USERS_MAX_DURATION_MINUTES * 60) { + context.log("timeout"); + break; + } + } + } + } catch (e) { + context.log("importUsers failed."); + context.error(e); + throw e; + } finally { + context.log(`[OUT] importUsersProcessing`); + } +} + +export async function importUsers( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log(`[IN] importUsers`); + try { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + + const blobstorageService = new BlobstorageService(); + const userApi = new UsersApi( + new Configuration({ + basePath: process.env.BASE_PATH, + }) + ); + + await importUsersProcessing(context, blobstorageService, userApi); + } catch (e) { + context.log("importUsers failed."); + context.error(e); + throw e; + } finally { + context.log(`[OUT] importUsers`); + } +} + +/** + * ODMS CloudのAPIを呼び出してユーザーを追加する + * @param context + * @param user + * @returns user + */ +export async function addUser( + context: InvocationContext, + userApi: UsersApi, + user: ImportData, + token: string +): Promise { + context.log(`[IN] addUser`); + try { + await userApi.signup( + { + email: user.email, + name: user.name, + role: RoleNumberMap[user.role], + autoRenew: user.auto_renew === 1, + notification: user.notification === 1, + authorId: user.role === 1 ? user.author_id : undefined, + encryption: user.role === 1 ? user.encryption === 1 : undefined, + encryptionPassword: + user.encryption === 1 ? user.encryption_password : undefined, + prompt: user.role === 1 ? user.prompt === 1 : undefined, + }, + { + headers: { authorization: `Bearer ${token}` }, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + } + ); + } catch (e) { + context.error(e); + context.error(JSON.stringify(e.response?.data)); + throw e; + } finally { + context.log(`[OUT] addUser`); + } +} +/** + * ユーザーのデータが正しいかどうかをチェック + * @param context + * @param user + * @param fileName + * @param row + * @returns true if user + */ +function checkUser( + context: InvocationContext, + user: ImportData, + fileName: string, + row: number +): boolean { + context.log( + `[IN] checkUser | params: { fileName: ${fileName}, row: ${row} }` + ); + try { + // 名前が255文字以内であること + if (user.name.length > 255) { + context.log(`name is too long. fileName: ${fileName}, row: ${row}`); + return false; + } + const emailPattern = + /^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/; + // メールアドレスが255文字以内であること + if (user.email.length > 255) { + context.log(`email is too long. fileName: ${fileName}, row: ${row}`); + return false; + } + if (!emailPattern.test(user.email)) { + context.log(`Invalid email. fileName: ${fileName}, row: ${row}`); + return false; + } + // ロールが(0/1/2)のいずれかであること + if (![0, 1, 2].includes(user.role)) { + context.log(`Invalid role number. fileName: ${fileName}, row: ${row}`); + return false; + } + // ロールがAuthorの場合 + if (user.role === 1) { + // author_idが必須 + if (user.author_id === undefined) { + context.log( + `author_id is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // author_idが16文字以内であること + if (user.author_id.length > 16) { + context.log( + `author_id is too long. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // author_idが半角大文字英数字とハイフンであること + if (!/^[A-Z0-9_]*$/.test(user.author_id)) { + context.log(`author_id is invalid. fileName: ${fileName}, row: ${row}`); + return false; + } + // encryptionが必須 + if (user.encryption === undefined) { + context.log( + `encryption is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // encryptionが1の場合 + if (user.encryption === 1) { + // encryption_passwordが必須 + if (user.encryption_password === undefined) { + context.log( + `encryption_password is required. fileName: ${fileName}, row: ${row}` + ); + return false; + } + // 4~16文字の半角英数字と記号のみであること + if (!/^[!-~]{4,16}$/.test(user.encryption_password)) { + context.log( + `encryption_password is invalid. fileName: ${fileName}, row: ${row}` + ); + return false; + } + if (user.prompt === undefined) { + context.log(`prompt is required. fileName: ${fileName}, row: ${row}`); + return false; + } + } + } + + return true; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] checkUser`); + } +} + +/** + * 代行操作用のアクセストークンを生成します + * @param context + * @param externalId + * @returns delegation token + */ +async function generateDelegationAccessToken( + context: InvocationContext, + externalId: string, + role: string +): Promise { + context.log( + `[IN] generateDelegationAccessToken | params: { externalId: ${externalId} }` + ); + try { + // 要求されたトークンの寿命を決定 + const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB); + const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? ""); + + const token = sign( + { + role: `${role} ${ADMIN_ROLES.ADMIN}`, + tier: TIERS.TIER5, + userId: externalId, + delegateUserId: SYSTEM_IMPORT_USERS, + }, + tokenLifetime, + privateKey + ); + + return token; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] generateDelegationAccessToken`); + } +} +/** + * System用のアクセストークンを生成します + * @param context + * @returns system token + */ +async function generateSystemToken( + context: InvocationContext +): Promise { + context.log(`[IN] generateSystemToken`); + try { + // 要求されたトークンの寿命を決定 + const tokenLifetime = Number(process.env.ACCESS_TOKEN_LIFETIME_WEB); + const privateKey = getJwtKey(process.env.JWT_PRIVATE_KEY ?? ""); + + const token = sign( + { + systemName: SYSTEM_IMPORT_USERS, + }, + tokenLifetime, + privateKey + ); + + return token; + } catch (e) { + context.error(e); + throw e; + } finally { + context.log(`[OUT] generateSystemToken`); + } +} + +const getCurrentUnixTime = () => Math.floor(new Date().getTime() / 1000); + +// 5分毎に実行 +app.timer("importUsers", { + schedule: "0 */5 * * * *", + handler: importUsers, +}); diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index f6b5f10..d1348fa 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -1,6 +1,5 @@ import { app, InvocationContext, Timer } from "@azure/functions"; import { Between, DataSource, In, IsNull, MoreThan, Not } from "typeorm"; -import { User } from "../entity/user.entity"; import { Account } from "../entity/account.entity"; import { ADB2C_SIGN_IN_TYPE, @@ -29,6 +28,7 @@ import { SEND_COMPLETE_PREFIX, DONE, } from "../common/cache/constants"; +import { initializeDataSource } from "../database/initializeDataSource"; export async function licenseAlertProcessing( context: InvocationContext, @@ -99,24 +99,7 @@ export async function licenseAlert( dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } - + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 diff --git a/dictation_function/src/functions/licenseAutoAllocation.ts b/dictation_function/src/functions/licenseAutoAllocation.ts index 0e08f36..8b1b053 100644 --- a/dictation_function/src/functions/licenseAutoAllocation.ts +++ b/dictation_function/src/functions/licenseAutoAllocation.ts @@ -22,6 +22,7 @@ import { DateWithZeroTime, NewAllocatedLicenseExpirationDate, } from "../common/types/types"; +import { initializeDataSource } from "../database/initializeDataSource"; import { readFileSync } from "fs"; import path from "path"; import { SendGridService } from "../sendgrid/sendgrid"; @@ -96,23 +97,7 @@ export async function licenseAutoAllocation( context.log("[IN]licenseAutoAllocation"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 diff --git a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts index 5864510..c4b4514 100644 --- a/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts +++ b/dictation_function/src/functions/licenseAutoAllocationManualRetry.ts @@ -7,11 +7,8 @@ import { } from "@azure/functions"; import { licenseAutoAllocationProcessing } from "./licenseAutoAllocation"; import * as dotenv from "dotenv"; -import { DataSource } from "typeorm"; -import { User } from "../entity/user.entity"; -import { Account } from "../entity/account.entity"; -import { License, LicenseAllocationHistory } from "../entity/license.entity"; import { HTTP_METHODS, HTTP_STATUS_CODES } from "../constants"; +import { initializeDataSource } from "../database/initializeDataSource"; import { RedisClient } from "redis"; import { createRedisClient } from "../redis/redis"; import { AdB2cService } from "../adb2c/adb2c"; @@ -44,23 +41,7 @@ export async function licenseAutoAllocationManualRetry( context.log("[IN]licenseAutoAllocationManualRetry"); dotenv.config({ path: ".env" }); dotenv.config({ path: ".env.local", override: true }); - let datasource: DataSource; - try { - datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License, LicenseAllocationHistory], - }); - await datasource.initialize(); - } catch (e) { - context.log("database initialize failed."); - context.error(e); - throw e; - } + const datasource = await initializeDataSource(context); let redisClient: RedisClient; try { // redis接続 @@ -72,7 +53,6 @@ export async function licenseAutoAllocationManualRetry( } const sendGrid = new SendGridService(); const adb2c = new AdB2cService(); - await licenseAutoAllocationProcessing( context, @@ -88,10 +68,12 @@ export async function licenseAutoAllocationManualRetry( body: "Automatic license allocation has been triggered.", }; } else { - context.log("Please use the POST method."); + context.log( + `Please use the POST method. Requested method = [${req.method}]` + ); return { status: HTTP_STATUS_CODES.BAD_REQUEST, - body: "Please use the POST method.", + body: `Please use the POST method. method = [${req.method}]`, }; } } catch (e) { diff --git a/dictation_function/src/test/analysisLicenses.spec.ts b/dictation_function/src/test/analysisLicenses.spec.ts new file mode 100644 index 0000000..56e2bea --- /dev/null +++ b/dictation_function/src/test/analysisLicenses.spec.ts @@ -0,0 +1,2830 @@ +import { DataSource } from "typeorm"; +import { + getBaseData, + getBaseDataFromDeletedAccounts, + outputAnalysisLicensesData, + transferData, +} from "../functions/analysisLicenses"; +import { + makeTestAccount, + makeTestUserArchive, + createLicense, + createLicenseAllocationHistory, + makeTestAccountArchive, + createLicenseArchive, + createLicenseAllocationHistoryArchive, + makeTestUser, +} from "./common/utility"; +import * as dotenv from "dotenv"; +import { + DateWithZeroTime, + ExpirationThresholdDate, +} from "../common/types/types"; +import { InvocationContext } from "@azure/functions"; +import { + LICENSE_ALLOCATED_STATUS, + LICENSE_COUNT_ANALYSIS_CATEGORY_1, + LICENSE_COUNT_ANALYSIS_CATEGORY_2, + LICENSE_COUNT_ANALYSIS_LICENSE_TYPE, + LICENSE_COUNT_ANALYSIS_ROLE, + SWITCH_FROM_TYPE, +} from "../constants"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { User, UserArchive } from "../entity/user.entity"; +describe("analysisLicenses", () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + let source: DataSource | null = null; + + beforeEach(async () => { + source = new DataSource({ + type: "sqlite", + database: ":memory:", + logging: false, + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: true, // trueにすると自動的にmigrationが行われるため注意 + }); + return source.initialize(); + }); + + afterEach(async () => { + if (!source) return; + await source.destroy(); + source = null; + }); + + it("getBaseData取得情報の確認", async () => { + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + + // tier4とtier5のアカウント+管理者を作る(tier4は対象外確認用) + // 第五アカウント:2件 + // 第五ユーザー:2件 + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin1" } + ); + const { account: account5_2, admin: admin5_2 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin2" } + ); + + // 削除ユーザを作成する + const userArchive5 = await makeTestUserArchive(source, { + account_id: account5_1.id, + }); + // 第五階層以外だとヒットしないことの確認 + const userArchive4 = await makeTestUserArchive(source, { + account_id: account4.id, + }); + + // 所有ライセンス + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:2か月前 + // ・期限:null or 14日後 + await createLicense( + source, + 1, + null, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 2, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 3, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 4, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // ※条件的に「所有ライセンス」にもカウントされる(+3) + await createLicense( + source, + 11, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 12, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 13, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 14, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + await createLicense( + source, + 21, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 22, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 23, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicense( + source, + 24, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // 先々月はヒットしないことの確認 + await createLicense( + source, + 25, + last2Month, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + await createLicenseAllocationHistory( + source, + 1, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 2, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 3, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.NONE + ); + // 先々月の登録ではヒットしないことの確認 + await createLicenseAllocationHistory( + source, + 4, + admin5_1.id, + 1, + true, + account5_1.id, + last2Month, + SWITCH_FROM_TYPE.TRIAL + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + expect(result.accountsAndUsersFromTier5).toHaveLength(2); + expect(result.accountsAndUsersFromTier5[0].id).toBe(account5_1.id); + expect(result.accountsAndUsersFromTier5[1].id).toBe(account5_2.id); + + expect(result.accountsAndUsersFromTier5[0].user).toHaveLength(1); + expect(result.accountsAndUsersFromTier5[1].user).toHaveLength(1); + if ( + result.accountsAndUsersFromTier5[0].user && + result.accountsAndUsersFromTier5[1].user + ) { + expect(result.accountsAndUsersFromTier5[0].user[0].id).toBe(admin5_1.id); + expect(result.accountsAndUsersFromTier5[1].user[0].id).toBe(admin5_2.id); + } else { + throw new Error("ユーザー取得できていないので失敗"); + } + + expect(result.accountsAndUsersFromTier5[0].userArchive).toHaveLength(1); + if (result.accountsAndUsersFromTier5[0].userArchive) { + expect(result.accountsAndUsersFromTier5[0].userArchive[0].id).toBe( + userArchive5.id + ); + } else { + throw new Error("ユーザー取得できていないので失敗"); + } + + expect(result.avairableLicenses).toHaveLength(6); + expect(result.avairableLicenses[0].id).toBe(1); + expect(result.avairableLicenses[1].id).toBe(2); + expect(result.avairableLicenses[2].id).toBe(3); + expect(result.avairableLicenses[3].id).toBe(11); + expect(result.avairableLicenses[4].id).toBe(12); + expect(result.avairableLicenses[5].id).toBe(13); + + expect(result.licensesIssuedInTargetMonth).toHaveLength(3); + expect(result.licensesIssuedInTargetMonth[0].id).toBe(11); + expect(result.licensesIssuedInTargetMonth[1].id).toBe(12); + expect(result.licensesIssuedInTargetMonth[2].id).toBe(13); + + expect(result.licensesExpiredInTargetMonth).toHaveLength(3); + expect(result.licensesExpiredInTargetMonth[0].id).toBe(21); + expect(result.licensesExpiredInTargetMonth[1].id).toBe(22); + expect(result.licensesExpiredInTargetMonth[2].id).toBe(23); + + expect(result.switchedlicensesInTargetMonth).toHaveLength(2); + expect(result.switchedlicensesInTargetMonth[0].id).toBe(1); + expect(result.switchedlicensesInTargetMonth[1].id).toBe(2); + }); + + it("getBaseDataFromDeletedAccounts取得情報の確認", async () => { + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + + // tier4とtier5のアカウント+管理者を作る(tier4は対象外確認用) + // 第五アカウント:2件 + // 第五ユーザー:2件 + const { account: account4, admin: admin4 } = await makeTestAccountArchive( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1 } = + await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin1" } + ); + const { account: account5_2, admin: admin5_2 } = + await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { external_id: "external_id_tier5admin2" } + ); + + // 所有ライセンス + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:2か月前 + // ・期限:null or 14日後 + await createLicenseArchive( + source, + 1, + null, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 2, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 3, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 4, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // ※条件的に「所有ライセンス」にもカウントされる(+3) + await createLicenseArchive( + source, + 11, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 12, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 13, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 14, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + await createLicenseArchive( + source, + 21, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 22, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 23, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // deleteはヒットしないことの確認 + await createLicenseArchive( + source, + 24, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.DELETED, + null, + null, + null, + null, + last2Month + ); + // 先々月はヒットしないことの確認 + await createLicenseArchive( + source, + 25, + last2Month, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + await createLicenseAllocationHistoryArchive( + source, + 1, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 2, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // SWITCH_FROM_TYPE.NONEではヒットしないことの確認 + await createLicenseAllocationHistoryArchive( + source, + 3, + admin5_1.id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.NONE + ); + // 先々月の登録ではヒットしないことの確認 + await createLicenseAllocationHistoryArchive( + source, + 4, + admin5_1.id, + 1, + true, + account5_1.id, + last2Month, + SWITCH_FROM_TYPE.TRIAL + ); + + const result = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); + expect(result.deletedAccountsAndUsersFromTier5).toHaveLength(2); + expect(result.deletedAccountsAndUsersFromTier5[0].id).toBe(account5_1.id); + expect(result.deletedAccountsAndUsersFromTier5[1].id).toBe(account5_2.id); + + expect(result.deletedAccountsAndUsersFromTier5[0].userArchive).toHaveLength( + 1 + ); + expect(result.deletedAccountsAndUsersFromTier5[1].userArchive).toHaveLength( + 1 + ); + if ( + result.deletedAccountsAndUsersFromTier5[0].userArchive && + result.deletedAccountsAndUsersFromTier5[1].userArchive + ) { + expect(result.deletedAccountsAndUsersFromTier5[0].userArchive[0].id).toBe( + admin5_1.id + ); + expect(result.deletedAccountsAndUsersFromTier5[1].userArchive[0].id).toBe( + admin5_2.id + ); + } + + expect(result.deletedAvairableLicenses).toHaveLength(6); + expect(result.deletedAvairableLicenses[0].id).toBe(1); + expect(result.deletedAvairableLicenses[1].id).toBe(2); + expect(result.deletedAvairableLicenses[2].id).toBe(3); + expect(result.deletedAvairableLicenses[3].id).toBe(11); + expect(result.deletedAvairableLicenses[4].id).toBe(12); + expect(result.deletedAvairableLicenses[5].id).toBe(13); + + expect(result.deletedLicensesIssuedInTargetMonth).toHaveLength(3); + expect(result.deletedLicensesIssuedInTargetMonth[0].id).toBe(11); + expect(result.deletedLicensesIssuedInTargetMonth[1].id).toBe(12); + expect(result.deletedLicensesIssuedInTargetMonth[2].id).toBe(13); + + expect(result.deletedLicensesExpiredInTargetMonth).toHaveLength(3); + expect(result.deletedLicensesExpiredInTargetMonth[0].id).toBe(21); + expect(result.deletedLicensesExpiredInTargetMonth[1].id).toBe(22); + expect(result.deletedLicensesExpiredInTargetMonth[2].id).toBe(23); + + expect(result.deletedSwitchedlicensesInTargetMonth).toHaveLength(2); + expect(result.deletedSwitchedlicensesInTargetMonth[0].id).toBe(1); + expect(result.deletedSwitchedlicensesInTargetMonth[1].id).toBe(2); + }); + + it("transferDataの確認", async () => { + // getBaseData取得情報の確認とgetBaseDataFromDeletedAccounts取得情報の確認 + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + const last2MonthYYYYMM = `${last2Month.getFullYear()}${( + last2Month.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // tier4とtier5のアカウント+管理者を作る + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + // 第五アカウントに紐づくユーザーを作成する + // 各ロール33人作成 + // users[0]~users[32] // author + // users[33]~users[65] // typist + // users[66]~users[98] // none + // author + const numberOfUsers = 33; + let users: User[] = []; + + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "author", + }); + if (user) { + users.push(user); + } + } + // typist + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "typist", + }); + if (user) { + users.push(user); + } + } + // none + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUser(source, { + account_id: account5_1.id, + role: "none", + }); + if (user) { + users.push(user); + } + } + + // 所有ライセンス + // usedTrialLicensesAuthorCount 1件 + await createLicense( + source, + 1, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[0].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesTypistCount 2件 + await createLicense( + source, + 2, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[33].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 3, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[34].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesNoneCount 3件 + await createLicense( + source, + 4, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[66].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 5, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[67].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 6, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[68].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesAuthorCount 4件 + await createLicense( + source, + 7, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[1].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 8, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[2].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 9, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[3].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 10, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[4].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesTypistCount 1件 + await createLicense( + source, + 11, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[35].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesNoneCount 2件 + await createLicense( + source, + 12, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[69].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 13, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[70].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesAuthorCount 2件 + await createLicense( + source, + 14, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[5].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 15, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[6].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesTypistCount 3件 + await createLicense( + source, + 16, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[36].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 17, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[37].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 18, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[38].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesNoneCount 1件 + await createLicense( + source, + 19, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[71].id, + null, + null, + null, + last2Month + ); + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // currentMonthIssuedTrialLicensesCount 3件 + await createLicense( + source, + 21, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 22, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 23, + expiringSoonDate, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedNormalLicensesCount 2件 + await createLicense( + source, + 24, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicense( + source, + 25, + expiringSoonDate, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedCardLicensesCount 1件 + await createLicense( + source, + 26, + expiringSoonDate, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + // invalidTrialLicensesAuthorCount 5件 + await createLicense( + source, + 27, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[7].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 28, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[8].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 29, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[9].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 30, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[10].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 31, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[11].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesTypistCount 3件 + await createLicense( + source, + 32, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[40].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 33, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[41].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 34, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[42].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesNoneCount 2件 + await createLicense( + source, + 35, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[74].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 36, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[75].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesUnallocatedCount 1件 + await createLicense( + source, + 37, + lastMonth, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesAuthorCount 2件 + await createLicense( + source, + 38, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[12].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 39, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[13].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesTypistCount 1件 + await createLicense( + source, + 40, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[43].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesNoneCount 2件 + await createLicense( + source, + 41, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[76].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 42, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[77].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesUnallocatedCount 2件 + await createLicense( + source, + 43, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 44, + lastMonth, + account5_1.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidCardLicensesAuthorCount 1件 + await createLicense( + source, + 45, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[14].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesTypistCount 2件 + await createLicense( + source, + 46, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[44].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 47, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[45].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesNoneCount 3件 + await createLicense( + source, + 48, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[78].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 49, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[79].id, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 50, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[80].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesUnallocatedCount 5件 + await createLicense( + source, + 51, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 52, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 53, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 54, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicense( + source, + 55, + lastMonth, + account5_1.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + // switchedTypistLicensesAuthorCount 1件 + await createLicenseAllocationHistory( + source, + 1, + users[1].id, + 1, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesTypistCount 2件 + await createLicenseAllocationHistory( + source, + 2, + users[34].id, + 2, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + await createLicenseAllocationHistory( + source, + 3, + users[35].id, + 3, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesNoneCount 1件 + await createLicenseAllocationHistory( + source, + 4, + users[66].id, + 4, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedNormalLicensesAuthorCount 1件 + await createLicenseAllocationHistory( + source, + 6, + users[5].id, + 6, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + // switchedNormalLicensesTypistCount 2件 + await createLicenseAllocationHistory( + source, + 7, + users[36].id, + 7, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 8, + users[37].id, + 8, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 5, + users[67].id, + 5, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 9, + users[69].id, + 9, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistory( + source, + 10, + users[70].id, + 10, + true, + account5_1.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + + // 削除されたアカウントとユーザーの情報を作成する + const { account: account5_1_D } = await makeTestAccountArchive( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + + // 第五アカウントに紐づくユーザーを作成する + // 各ロール33人作成 + // users[0]~users[32] // author + // users[33]~users[65] // typist + // users[66]~users[98] // none + // author + let deleteUsers: UserArchive[] = []; + + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "author", + }); + if (user) { + deleteUsers.push(user); + } + } + // typist + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "typist", + }); + if (user) { + deleteUsers.push(user); + } + } + // none + for (let i = 1; i <= numberOfUsers; i++) { + const user = await makeTestUserArchive(source, { + account_id: account5_1_D.id, + role: "none", + }); + if (user) { + deleteUsers.push(user); + } + } + // 所有ライセンス + // usedTrialLicensesAuthorCount 1件 + await createLicenseArchive( + source, + 1, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[0].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesTypistCount 2件 + await createLicenseArchive( + source, + 2, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[33].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 3, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[34].id, + null, + null, + null, + last2Month + ); + // usedTrialLicensesNoneCount 3件 + await createLicenseArchive( + source, + 4, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[66].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 5, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[67].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 6, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[68].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesAuthorCount 4件 + await createLicenseArchive( + source, + 7, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[1].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 8, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[2].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 9, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[3].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 10, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[4].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesTypistCount 1件 + await createLicenseArchive( + source, + 11, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[35].id, + null, + null, + null, + last2Month + ); + // usedNormalLicensesNoneCount 2件 + await createLicenseArchive( + source, + 12, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[69].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 13, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[70].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesAuthorCount 2件 + await createLicenseArchive( + source, + 14, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[5].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 15, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[6].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesTypistCount 3件 + await createLicenseArchive( + source, + 16, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[36].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 17, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[37].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 18, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[38].id, + null, + null, + null, + last2Month + ); + // usedCardLicensesNoneCount 1件 + await createLicenseArchive( + source, + 19, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[71].id, + null, + null, + null, + last2Month + ); + // その月に発行したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から1か月前 + // ・期限:14日後 + // currentMonthIssuedTrialLicensesCount 3件 + await createLicenseArchive( + source, + 21, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 22, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 23, + expiringSoonDate, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedNormalLicensesCount 2件 + await createLicenseArchive( + source, + 24, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + await createLicenseArchive( + source, + 25, + expiringSoonDate, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + // currentMonthIssuedCardLicensesCount 1件 + await createLicenseArchive( + source, + 26, + expiringSoonDate, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + lastMonth + ); + + // その月に失効したライセンスを作成 + // 条件: + // ・第五アカウント + // ・ステータス:ALLOCATED/REUSABLE/UNALLOCATED + // ・作成日:今日から2か月前 + // ・期限:先月 + // invalidTrialLicensesAuthorCount 5件 + await createLicenseArchive( + source, + 27, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[7].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 28, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[8].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 29, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[9].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 30, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[10].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 31, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[11].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesTypistCount 3件 + await createLicenseArchive( + source, + 32, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[40].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 33, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[41].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 34, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[42].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesNoneCount 2件 + await createLicenseArchive( + source, + 35, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[74].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 36, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[75].id, + null, + null, + null, + last2Month + ); + // invalidTrialLicensesUnallocatedCount 1件 + await createLicenseArchive( + source, + 37, + lastMonth, + account5_1_D.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesAuthorCount 2件 + await createLicenseArchive( + source, + 38, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[12].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 39, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[13].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesTypistCount 1件 + await createLicenseArchive( + source, + 40, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[43].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesNoneCount 2件 + await createLicenseArchive( + source, + 41, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[76].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 42, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[77].id, + null, + null, + null, + last2Month + ); + // invalidNormalLicensesUnallocatedCount 2件 + await createLicenseArchive( + source, + 43, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 44, + lastMonth, + account5_1_D.id, + "NORMAL", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + // invalidCardLicensesAuthorCount 1件 + await createLicenseArchive( + source, + 45, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[14].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesTypistCount 2件 + await createLicenseArchive( + source, + 46, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[44].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 47, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[45].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesNoneCount 3件 + await createLicenseArchive( + source, + 48, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[78].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 49, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[79].id, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 50, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + users[80].id, + null, + null, + null, + last2Month + ); + // invalidCardLicensesUnallocatedCount 5件 + await createLicenseArchive( + source, + 51, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 52, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 53, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 54, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + await createLicenseArchive( + source, + 55, + lastMonth, + account5_1_D.id, + "CARD", + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + last2Month + ); + + // 第五階層がその月におこなったライセンス切り替え情報を作成 + // 条件: + // ・第五アカウント + // ・実行日時:先月 + // ・切り替えタイプ:CARD/TRIAL + // switchedTypistLicensesAuthorCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 1, + users[1].id, + 1, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesTypistCount 2件 + await createLicenseAllocationHistoryArchive( + source, + 2, + users[34].id, + 2, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + await createLicenseAllocationHistoryArchive( + source, + 3, + users[35].id, + 3, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedTypistLicensesNoneCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 4, + users[66].id, + 4, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.TRIAL + ); + // switchedNormalLicensesAuthorCount 1件 + await createLicenseAllocationHistoryArchive( + source, + 6, + users[5].id, + 6, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + // switchedNormalLicensesTypistCount 2件 + await createLicenseAllocationHistoryArchive( + source, + 7, + users[36].id, + 7, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 8, + users[37].id, + 8, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 5, + users[67].id, + 5, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 9, + users[69].id, + 9, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + await createLicenseAllocationHistoryArchive( + source, + 10, + users[70].id, + 10, + true, + account5_1_D.id, + lastMonth, + SWITCH_FROM_TYPE.CARD + ); + const result_D = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); + const transferDataResult = await transferData( + context, + result, + result_D, + lastMonthYYYYMM + ); + let csvContentUS = ""; + for (let i = 0; i < transferDataResult.outputDataUS.length; i++) { + //カンマ区切りの文字列を作成 + csvContentUS += transferDataResult.outputDataUS[i]; + } + expect(csvContentUS).toBe( + '"アカウント","対象年月","カテゴリー1","カテゴリー2","ライセンス種別","役割","数量"' + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + + "\r\n" + + `"test inc.","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Trial","","9"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Standard","","9"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","所有ライセンス数","Card","","7"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Author","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","Typist","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Trial","None","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Author","4"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","Typist","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Standard","None","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Author","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","Typist","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","使用中ライセンス数","Card","None","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Trial","","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Standard","","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","新規発行ライセンス数","","Card","","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Author","5"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Typist","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","None","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Trial","Unallocated","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Author","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Typist","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","None","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Standard","Unallocated","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Author","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Typist","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","None","3"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","失効ライセンス数","","Card","Unallocated","5"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Author","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","Typist","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","トライアルから切り替え","None","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Author","1"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","Typist","2"` + + "\r\n" + + `"1","${lastMonthYYYYMM}","有効ライセンス数","","カードから切り替え","None","3"` + + "\r\n" + ); + }); + + it("outputDataの確認(Mock)", async () => { + const blobService = new BlobstorageService(); + + if (!source) fail(); + const context = new InvocationContext(); + + const currentDate = new DateWithZeroTime(); + + // 現在の日付を取得 + const nowDate = new Date(); + + // 先月の日付を取得 + const lastMonth = new Date(nowDate); + lastMonth.setMonth(nowDate.getMonth() - 1); + const lastMonthYYYYMM = `${lastMonth.getFullYear()}${( + lastMonth.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // 先々月の日付を取得 + const last2Month = new Date(nowDate); + last2Month.setMonth(nowDate.getMonth() - 2); + const last2MonthYYYYMM = `${last2Month.getFullYear()}${( + last2Month.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}`; + + // tier4とtier5のアカウント+管理者を作る + const { account: account4, admin: admin4 } = await makeTestAccount( + source, + { tier: 4 }, + { external_id: "external_id_tier4admin" } + ); + const { account: account5_1, admin: admin5_1_1 } = await makeTestAccount( + source, + { + tier: 5, + parent_account_id: account4.id, + }, + { + external_id: "external_id_tier5admin1", + role: "author", + } + ); + + // 所有ライセンス + // usedTrialLicensesAuthorCount 1件 + await createLicense( + source, + 1, + null, + account5_1.id, + "TRIAL", + LICENSE_ALLOCATED_STATUS.ALLOCATED, + admin5_1_1.id, + null, + null, + null, + last2Month + ); + + const result = await getBaseData(context, lastMonthYYYYMM, source); + const result_D = await getBaseDataFromDeletedAccounts( + context, + lastMonthYYYYMM, + source + ); + const transferDataResult = await transferData( + context, + result, + result_D, + lastMonthYYYYMM + ); + const mockUploadFileAnalysisLicensesCSV = jest.fn().mockReturnValue(true); + const mockCreateContainerAnalysis = jest.fn().mockReturnValue(true); + blobService.uploadFileAnalysisLicensesCSV = + mockUploadFileAnalysisLicensesCSV; + blobService.createContainerAnalysis = mockCreateContainerAnalysis; + + const resultOutputData = await outputAnalysisLicensesData( + context, + blobService, + transferDataResult + ); + expect(resultOutputData).toEqual(true); + }); +}); diff --git a/dictation_function/src/test/common/context.ts b/dictation_function/src/test/common/context.ts new file mode 100644 index 0000000..7d2f017 --- /dev/null +++ b/dictation_function/src/test/common/context.ts @@ -0,0 +1,16 @@ +import { InvocationContext } from "@azure/functions"; + +export class TestInvocationContext extends InvocationContext { + contents: string[] = []; + getLogs(): string[] { + return this.contents; + } + log(...args: any[]): void { + super.log(args); + this.contents.push(args.toString()); + } + error(...args: any[]): void { + super.error(args); + this.contents.push(args.toString()); + } +} diff --git a/dictation_function/src/test/common/init.ts b/dictation_function/src/test/common/init.ts new file mode 100644 index 0000000..bfe1512 --- /dev/null +++ b/dictation_function/src/test/common/init.ts @@ -0,0 +1,21 @@ +import { DataSource } from 'typeorm'; + +export const truncateAllTable = async (source: DataSource) => { + const entities = source.entityMetadatas; + const queryRunner = source.createQueryRunner(); + + try { + await queryRunner.startTransaction(); + await queryRunner.query('SET FOREIGN_KEY_CHECKS=0'); + for (const entity of entities) { + await queryRunner.query(`TRUNCATE TABLE \`${entity.tableName}\``); + } + await queryRunner.query('SET FOREIGN_KEY_CHECKS=1'); + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw err; + } finally { + await queryRunner.release(); + } +}; diff --git a/dictation_function/src/test/common/logger.ts b/dictation_function/src/test/common/logger.ts new file mode 100644 index 0000000..19f5124 --- /dev/null +++ b/dictation_function/src/test/common/logger.ts @@ -0,0 +1,129 @@ +import { Logger, QueryRunner } from "typeorm"; +import * as fs from "fs"; +import * as path from "path"; + +interface IOutput { + initialize(): void; + write(message: string): void; +} + +class ConsoleOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + console.log(message); + } +} + +class FileOutput implements IOutput { + private logPath = path.join("/app/dictation_function/.test", "logs"); + private fileName = new Date().getTime(); + + initialize(): void { + if (!fs.existsSync(this.logPath)) { + fs.mkdirSync(this.logPath, { recursive: true }); + } + } + + write(message: string): void { + const logFile = path.join(this.logPath, `${this.fileName}.log`); + fs.appendFileSync(logFile, `${message}\n`); + } +} + +class NoneOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + // do nothing + } +} + +export class TestLogger implements Logger { + out: IOutput; + + constructor(output: "none" | "file" | "console") { + switch (output) { + case "none": + this.out = new NoneOutput(); + break; + case "file": + this.out = new FileOutput(); + break; + case "console": + this.out = new ConsoleOutput(); + break; + default: + this.out = new NoneOutput(); + break; + } + this.out.initialize(); + } + + private write(message: string): void { + this.out.write(message); + } + + logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { + const raw = `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`; + // ex: 2024-03-08T06:38:43.125Z を TIME という文字列に置換 + const dateRemoved = raw.replace( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, + "TIME" + ); + // ex: /* コメント内容 */ を /* コメント */ という文字列に置換 + const commentRemoved = dateRemoved.replace( + /\/\*.*\*\//g, + "/* RequestID */" + ); + + // UUIDを固定文字列に置換する ex: 88a9c78e-115a-439c-9e23-731d649f0c27 を XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX という文字列に置換 + const uuidRemoved = commentRemoved.replace( + /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, + "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + ); + this.write(uuidRemoved); + } + + logQueryError( + error: string, + query: string, + parameters?: any[], + queryRunner?: QueryRunner + ) { + this.write( + `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters + )}` + ); + } + + logQuerySlow( + time: number, + query: string, + parameters?: any[], + queryRunner?: QueryRunner + ) { + this.write( + `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters + )}` + ); + } + + logSchemaBuild(message: string, queryRunner?: QueryRunner) { + this.write(`Schema Build: ${message}`); + } + + logMigration(message: string, queryRunner?: QueryRunner) { + this.write(`Migration: ${message}`); + } + + log(level: "log" | "info" | "warn", message: any, queryRunner?: QueryRunner) { + this.write(`${level.toUpperCase()}: ${message}`); + } +} diff --git a/dictation_function/src/test/common/utility.ts b/dictation_function/src/test/common/utility.ts index 7cec394..fe13688 100644 --- a/dictation_function/src/test/common/utility.ts +++ b/dictation_function/src/test/common/utility.ts @@ -1,9 +1,19 @@ import { v4 as uuidv4 } from "uuid"; -import { DataSource } from "typeorm"; -import { User } from "../../entity/user.entity"; -import { Account } from "../../entity/account.entity"; -import { ADMIN_ROLES, USER_ROLES } from "../../constants"; -import { License, LicenseAllocationHistory } from "../../entity/license.entity"; +import { DataSource, In } from "typeorm"; +import { User, UserArchive } from "../../entity/user.entity"; +import { Account, AccountArchive } from "../../entity/account.entity"; +import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from "../../constants"; +import { + License, + LicenseAllocationHistory, + LicenseArchive, + LicenseAllocationHistoryArchive, +} from "../../entity/license.entity"; +import { bigintTransformer } from "../../common/entity"; +import { Task } from "../../entity/task.entity"; +import { AudioFile } from "../../entity/audio_file.entity"; +import { AudioOptionItem } from "../../entity/audio_option_item.entity"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -25,8 +35,28 @@ type OverrideUser = Omit< "id" | "account" | "license" | "userGroupMembers" >; +type OverrideAccountArchive = Omit< + AccountArchive, + "id" | "primary_admin_user_id" | "secondary_admin_user_id" | "user" +>; + +// 上書きされたら困る項目を除外したUser型 +type OverrideUserArchive = Omit< + UserArchive, + "id" | "account" | "license" | "userGroupMembers" +>; + +type OverrideTask = Omit; + type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] }; type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] }; +type AccountArchiveDefault = { + [K in keyof OverrideAccountArchive]?: OverrideAccountArchive[K]; +}; +type UserArchiveDefault = { + [K in keyof OverrideUserArchive]?: OverrideUserArchive[K]; +}; +type TaskDefault = { [K in keyof OverrideTask]?: OverrideTask[K] }; /** * テスト ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する @@ -50,7 +80,6 @@ export const makeTestUser = async ( auto_renew: d?.auto_renew ?? true, notification: d?.notification ?? true, encryption: d?.encryption ?? true, - encryption_password: d?.encryption_password, prompt: d?.prompt ?? true, created_by: d?.created_by ?? "test_runner", created_at: d?.created_at ?? new Date(), @@ -96,6 +125,8 @@ export const makeTestAccount = async ( locked: d?.locked ?? false, company_name: d?.company_name ?? "test inc.", verified: d?.verified ?? true, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: d?.file_retention_days ?? 30, deleted_at: d?.deleted_at ?? null, created_by: d?.created_by ?? "test_runner", created_at: d?.created_at ?? new Date(), @@ -103,14 +134,14 @@ export const makeTestAccount = async ( updated_at: d?.updated_at ?? new Date(), }); const result = identifiers.pop() as Account; - accountId = result.id; + accountId = bigintTransformer.from(result.id); } { const d = defaultAdminUserValue; const { identifiers } = await datasource.getRepository(User).insert({ external_id: d?.external_id ?? uuidv4(), account_id: accountId, - role: d?.role ?? "admin none", + role: d?.role ?? "none", author_id: d?.author_id ?? undefined, accepted_eula_version: d?.accepted_eula_version !== undefined @@ -126,7 +157,6 @@ export const makeTestAccount = async ( auto_renew: d?.auto_renew ?? true, notification: d?.notification ?? true, encryption: d?.encryption ?? true, - encryption_password: d?.encryption_password ?? "password", prompt: d?.prompt ?? true, deleted_at: d?.deleted_at ?? "", created_by: d?.created_by ?? "test_runner", @@ -136,7 +166,7 @@ export const makeTestAccount = async ( }); const result = identifiers.pop() as User; - userId = result.id; + userId = bigintTransformer.from(result.id); } // Accountの管理者を設定する @@ -173,7 +203,7 @@ export const makeTestAccount = async ( }; }; -export const createAndAllocateLicense = async ( +export const createLicense = async ( datasource: DataSource, licenseId: number, expiry_date: Date | null, @@ -183,7 +213,8 @@ export const createAndAllocateLicense = async ( allocated_user_id: number | null, order_id: number | null, deleted_at: Date | null, - delete_order_id: number | null + delete_order_id: number | null, + created_at?: Date ): Promise => { const { identifiers } = await datasource.getRepository(License).insert({ id: licenseId, @@ -196,7 +227,37 @@ export const createAndAllocateLicense = async ( deleted_at: deleted_at, delete_order_id: delete_order_id, created_by: "test_runner", - created_at: new Date(), + created_at: created_at ? created_at : new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as License; +}; +export const createAndAllocateLicense = async ( + datasource: DataSource, + licenseId: number, + expiry_date: Date | null, + accountId: number, + type: string, + status: string, + allocated_user_id: number | null, + order_id: number | null, + deleted_at: Date | null, + delete_order_id: number | null, + created_at?: Date +): Promise => { + const { identifiers } = await datasource.getRepository(License).insert({ + id: licenseId, + expiry_date: expiry_date, + account_id: accountId, + type: type, + status: status, + allocated_user_id: allocated_user_id, + order_id: order_id, + deleted_at: deleted_at, + delete_order_id: delete_order_id, + created_by: "test_runner", + created_at: created_at ? created_at : new Date(), updated_by: "updater", updated_at: new Date(), }); @@ -226,6 +287,34 @@ export const createAndAllocateLicense = async ( }); }; +export const createLicenseAllocationHistory = async ( + datasource: DataSource, + id: number, + user_id: number, + license_id: number, + is_allocated: boolean, + account_id: number, + executed_at: Date, + switch_from_type: string +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseAllocationHistory) + .insert({ + id: id, + user_id: user_id, + license_id: license_id, + is_allocated: is_allocated, + account_id: account_id, + executed_at: executed_at, + switch_from_type: switch_from_type, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseAllocationHistory; +}; + export const selectLicenseByAllocatedUser = async ( datasource: DataSource, userId: number @@ -256,3 +345,374 @@ export const selectLicenseAllocationHistory = async ( }); return { licenseAllocationHistory }; }; + +/** + * テスト ユーティリティ: 指定したプロパティを上書きしたアーカイブユーザーを作成する + * @param dataSource データソース + * @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト + * @returns 作成したユーザー + */ +export const makeTestUserArchive = async ( + datasource: DataSource, + defaultUserValue?: UserArchiveDefault +): Promise => { + const d = defaultUserValue; + const { identifiers } = await datasource.getRepository(UserArchive).insert({ + account_id: d?.account_id ?? -1, + external_id: d?.external_id ?? uuidv4(), + role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`, + author_id: d?.author_id, + accepted_eula_version: d?.accepted_eula_version ?? "1.0", + accepted_dpa_version: d?.accepted_dpa_version ?? "1.0", + email_verified: d?.email_verified ?? true, + auto_renew: d?.auto_renew ?? true, + notification: d?.notification ?? true, + encryption: d?.encryption ?? true, + prompt: d?.prompt ?? true, + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result = identifiers.pop() as User; + + const userArchive = await datasource.getRepository(UserArchive).findOne({ + where: { + id: result.id, + }, + }); + if (!userArchive) { + throw new Error("Unexpected null"); + } + return userArchive; +}; + +/** + * テスト ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する + * @param dataSource データソース + * @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト + * @param defaultAdminUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト(account_id等の所属関係が破壊される上書きは無視する) + * @returns 作成したアカウント + */ +export const makeTestAccountArchive = async ( + datasource: DataSource, + defaultAccountValue?: AccountArchiveDefault, + defaultAdminUserValue?: UserArchiveDefault, + isPrimaryAdminNotExist?: boolean, + isSecondaryAdminNotExist?: boolean +): Promise<{ account: AccountArchive; admin: UserArchive }> => { + let accountId: number; + let userId: number; + { + const d = defaultAccountValue; + const { identifiers } = await datasource + .getRepository(AccountArchive) + .insert({ + tier: d?.tier ?? 1, + parent_account_id: d?.parent_account_id ?? undefined, + country: d?.country ?? "US", + delegation_permission: d?.delegation_permission ?? false, + locked: d?.locked ?? false, + verified: d?.verified ?? true, + deleted_at: d?.deleted_at ?? "", + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result = identifiers.pop() as AccountArchive; + accountId = result.id; + } + { + const d = defaultAdminUserValue; + const { identifiers } = await datasource.getRepository(UserArchive).insert({ + external_id: d?.external_id ?? uuidv4(), + account_id: accountId, + role: d?.role ?? "admin none", + author_id: d?.author_id ?? undefined, + accepted_eula_version: d?.accepted_eula_version ?? "1.0", + accepted_dpa_version: d?.accepted_dpa_version ?? "1.0", + email_verified: d?.email_verified ?? true, + auto_renew: d?.auto_renew ?? true, + notification: d?.notification ?? true, + encryption: d?.encryption ?? true, + prompt: d?.prompt ?? true, + deleted_at: d?.deleted_at ?? "", + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + + const result = identifiers.pop() as UserArchive; + userId = result.id; + } + + // Accountの管理者を設定する + let secondaryAdminUserId: number | null = null; + if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) { + secondaryAdminUserId = userId; + } + await datasource.getRepository(AccountArchive).update( + { id: accountId }, + { + primary_admin_user_id: isPrimaryAdminNotExist ? null : userId, + secondary_admin_user_id: secondaryAdminUserId, + } + ); + + const account = await datasource.getRepository(AccountArchive).findOne({ + where: { + id: accountId, + }, + }); + + const admin = await datasource.getRepository(UserArchive).findOne({ + where: { + id: userId, + }, + }); + if (!account || !admin) { + throw new Error("Unexpected null"); + } + + return { + account: account, + admin: admin, + }; +}; + +export const createLicenseArchive = async ( + datasource: DataSource, + licenseId: number, + expiry_date: Date | null, + accountId: number, + type: string, + status: string, + allocated_user_id: number | null, + order_id: number | null, + deleted_at: Date | null, + delete_order_id: number | null, + created_at?: Date +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseArchive) + .insert({ + id: licenseId, + expiry_date: expiry_date, + account_id: accountId, + type: type, + status: status, + allocated_user_id: allocated_user_id, + order_id: order_id, + deleted_at: deleted_at, + delete_order_id: delete_order_id, + created_by: "test_runner", + created_at: created_at ? created_at : new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseArchive; +}; + +export const createLicenseAllocationHistoryArchive = async ( + datasource: DataSource, + id: number, + user_id: number, + license_id: number, + is_allocated: boolean, + account_id: number, + executed_at: Date, + switch_from_type: string +): Promise => { + const { identifiers } = await datasource + .getRepository(LicenseAllocationHistoryArchive) + .insert({ + id: id, + user_id: user_id, + license_id: license_id, + is_allocated: is_allocated, + account_id: account_id, + executed_at: executed_at, + switch_from_type: switch_from_type, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }); + identifiers.pop() as LicenseAllocationHistoryArchive; +}; + +export const makeTestTask = async ( + datasource: DataSource, + accountId: number, + ownerUserId: number, + identifierName: string, + defaultTaskValue?: TaskDefault +): Promise<{ task: Task; file: AudioFile; options: AudioOptionItem[] }> => { + const d = defaultTaskValue; + + // AudioFileを作成 + const { identifiers: identifiers1 } = await datasource + .getRepository(AudioFile) + .insert({ + account_id: accountId, + owner_user_id: ownerUserId, + url: `https://example.com/${identifierName}`, + file_name: `test${identifierName}.wav`, + raw_file_name: `test${identifierName}.wav`, + author_id: "test_author", + work_type_id: "test_work_type", + started_at: new Date(), + duration: "00:00:00", + finished_at: new Date(), + uploaded_at: new Date(), + file_size: 1024, + priority: "01", + audio_format: "wav", + comment: `test_comment_${identifierName}`, + deleted_at: new Date(), + is_encrypted: false, + }); + + const result = identifiers1.pop() as AudioFile; + const audioFileId = bigintTransformer.from(result.id); + + // AudioFileを取得 + const file = await datasource.getRepository(AudioFile).findOne({ + where: { + id: audioFileId, + }, + }); + if (!file) { + throw new Error("Unexpected null"); + } + + // Taskを作成 + const { identifiers: identifiers2 } = await datasource + .getRepository(Task) + .insert({ + job_number: d?.job_number ?? "0001", + account_id: accountId, + is_job_number_enabled: d?.is_job_number_enabled ?? true, + audio_file_id: file.id, + status: d?.status ?? "Uploaded", + typist_user_id: d?.typist_user_id, + priority: d?.priority ?? "01", + template_file_id: d?.template_file_id, + started_at: d?.started_at ?? new Date(), + finished_at: d?.finished_at ?? new Date(), + created_by: d?.created_by ?? "test_runner", + created_at: d?.created_at ?? new Date(), + updated_by: d?.updated_by ?? "updater", + updated_at: d?.updated_at ?? new Date(), + }); + const result2 = identifiers2.pop() as Task; + const taskId = bigintTransformer.from(result2.id); + + const task = await datasource.getRepository(Task).findOne({ + where: { + id: taskId, + }, + }); + if (!task) { + throw new Error("Unexpected null"); + } + + // AudioOptionItemを作成 + const item01 = await datasource.getRepository(AudioOptionItem).insert({ + audio_file_id: audioFileId, + label: `test_option_label_${identifierName}_01`, + value: `test_option_value_${identifierName}_01`, + }); + const item02 = await datasource.getRepository(AudioOptionItem).insert({ + audio_file_id: audioFileId, + label: `test_option_label_${identifierName}_02`, + value: `test_option_value_${identifierName}_02`, + }); + const optionItemResult01 = item01.identifiers.pop() as AudioOptionItem; + const optionItemResult02 = item02.identifiers.pop() as AudioOptionItem; + const optionItemID01 = bigintTransformer.from(optionItemResult01.id); + const optionItemID02 = bigintTransformer.from(optionItemResult02.id); + + const optionItems = await datasource.getRepository(AudioOptionItem).find({ + where: { + id: In([optionItemID01, optionItemID02]), + }, + }); + + return { task, file, options: optionItems }; +}; + +export const makeManyTestTasks = async ( + datasource: DataSource, + inputFiles: QueryDeepPartialEntity[], + task_finished_at: Date +): Promise => { + const fileRepository = datasource.getRepository(AudioFile); + const result = await fileRepository.insert(inputFiles); + const audioFileIds = result.identifiers.map((id) => id.id); + const files = await fileRepository.find({ + where: { + id: In(audioFileIds), + }, + }); + + const tasks = files.map((file, index): QueryDeepPartialEntity => { + return { + job_number: `0001_${index}`, + account_id: file.account_id, + is_job_number_enabled: true, + audio_file_id: file.id, + status: TASK_STATUS.FINISHED, + typist_user_id: null, + priority: "01", + template_file_id: null, + started_at: new Date(), + finished_at: task_finished_at, + created_by: "test_runner", + created_at: new Date(), + updated_by: "updater", + updated_at: new Date(), + }; + }); + + const taskRepository = datasource.getRepository(Task); + const x = await taskRepository.insert(tasks); + + const partialOptions = files.flatMap( + (file, index): QueryDeepPartialEntity[] => { + return [ + { + audio_file_id: file.id, + label: `test_option_label_${index}_01`, + value: `test_option_value_${index}_01`, + }, + { + audio_file_id: file.id, + label: `test_option_label_${index}_02`, + value: `test_option_value_${index}_02`, + }, + ]; + } + ); + + const optionRepository = datasource.getRepository(AudioOptionItem); + await optionRepository.insert(partialOptions); +}; + +export const getTasks = async (datasource: DataSource): Promise => { + return await datasource.getRepository(Task).find(); +}; + +export const getAudioFiles = async ( + datasource: DataSource +): Promise => { + return await datasource.getRepository(AudioFile).find(); +}; + +export const getAudioOptionItems = async ( + datasource: DataSource +): Promise => { + return await datasource.getRepository(AudioOptionItem).find(); +}; diff --git a/dictation_function/src/test/deleteAudioFiles.spec.ts b/dictation_function/src/test/deleteAudioFiles.spec.ts new file mode 100644 index 0000000..0b3f18a --- /dev/null +++ b/dictation_function/src/test/deleteAudioFiles.spec.ts @@ -0,0 +1,1630 @@ +import * as dotenv from "dotenv"; +import { InvocationContext } from "@azure/functions"; +import { DataSource } from "typeorm"; +import { truncateAllTable } from "./common/init"; +import { + deleteAudioFilesProcessing, + deleteRecords, + getProcessTargets, +} from "../functions/deleteAudioFiles"; +import { + getAudioFiles, + getAudioOptionItems, + getTasks, + makeManyTestTasks, + makeTestAccount, + makeTestTask, +} from "./common/utility"; +import { MANUAL_RECOVERY_REQUIRED, TASK_STATUS } from "../constants"; +import { TestLogger } from "./common/logger"; +import { AudioFile } from "../entity/audio_file.entity"; +import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; +import { AudioBlobStorageService } from "../blobstorage/audioBlobStorage.service"; +import { User, UserArchive } from "../entity/user.entity"; +import { Account, AccountArchive } from "../entity/account.entity"; +import { Task } from "../entity/task.entity"; +import { + License, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + LicenseArchive, +} from "../entity/license.entity"; +import { AudioOptionItem } from "../entity/audio_option_item.entity"; +import { TestInvocationContext } from "./common/context"; + +describe("getProcessTargets | 削除対象を特定するQueryが正常に動作するか確認する", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("ファイル削除対象のタスクが存在しない場合、空の配列が取得できる", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 2, + auto_file_delete: true, + }); + + { + // ファイル削除対象のタスクが存在しない場合、空の配列が取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([]); + } + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスクが存在しない場合、空の配列が取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(対象となる期限切れのタスク情報のみが取れる)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 2, + auto_file_delete: true, + }); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + // 2日と1秒前(削除対象)のタスクを作成 + const { task, file } = await makeTestTask( + source, + account.id, + admin.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task2, file: file2 } = await makeTestTask( + source, + account.id, + admin.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task3, file: file3 } = await makeTestTask( + source, + account.id, + admin.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + raw_file_name: file3.raw_file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account.id, admin.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account.id, + country: account.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + raw_file_name: file3.raw_file_name, + account_id: account.id, + country: account.country, + }, + ]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(auto_file_delete=falseのアカウントの情報のタスク情報は取れない)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // auto_file_deleteがfalseののアカウントを作成 + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task, file } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + { + // ファイル削除対象のタスク情報1件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task2, file: file2 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task3, file: file3 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account02.id, admin02.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + raw_file_name: file3.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報3件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task.id, + audio_file_id: file.id, + raw_file_name: file.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task2.id, + audio_file_id: file2.id, + raw_file_name: file2.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task3.id, + audio_file_id: file3.id, + raw_file_name: file3.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + ]); + } + }); + + it("ファイル削除対象のタスク情報のみを取得できる(auto_file_delete=trueのアカウントの情報のタスク情報は全て取れる)", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + // auto_file_deleteがfalseののアカウントを作成 + const { account: account03, admin: admin03 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task: task01, file: file01 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + const { task: task02, file: file02 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + { + // ファイル削除対象のタスク情報2件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task03, file: file03 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + const { task: task04, file: file04 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報4件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + raw_file_name: file03.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + raw_file_name: file04.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報4件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + raw_file_name: file03.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + raw_file_name: file04.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task05, file: file05 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + const { task: task06, file: file06 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + raw_file_name: file03.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + raw_file_name: file04.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + raw_file_name: file05.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + raw_file_name: file06.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + raw_file_name: file03.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + raw_file_name: file04.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + raw_file_name: file05.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + raw_file_name: file06.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + } + }); +}); + +describe("deleteRecords | 削除対象タスク等を削除できる", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [ + User, + UserArchive, + Account, + AccountArchive, + Task, + AudioFile, + AudioOptionItem, + License, + LicenseArchive, + LicenseAllocationHistory, + LicenseAllocationHistoryArchive, + ], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("競合により存在しないタスクを削除しようとしても例外は発生せず、成功扱いとなる", async () => { + if (!source) fail(); + + await deleteRecords(source, [ + { + id: 1, + audio_file_id: 1, + raw_file_name: "test", + account_id: 1, + country: "US", + }, + ]); + }); + + it("対象としたタスクやAudioFile等が削除される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // auto_file_deleteがtrueののアカウントを作成 + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + // auto_file_deleteがfalseののアカウントを作成 + const { account: account03, admin: admin03 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: false, + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 2日と1秒前(削除対象)のタスクを作成 + const { task: task01, file: file01 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + const { task: task02, file: file02 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + // ちょうど3日前(削除対象)のタスクを作成 + const { task: task03, file: file03 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + const { task: task04, file: file04 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case04", { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T00:00:00Z"), + }); + + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + await makeTestTask(source, account03.id, admin03.id, "case05", { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + + // 100日と5時間前(削除対象)のタスクを作成 + const { task: task05, file: file05 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + const { task: task06, file: file06 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case06", + { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + } + ); + await makeTestTask(source, account03.id, admin03.id, "case06", { + status: TASK_STATUS.FINISHED, + job_number: "job06", + finished_at: new Date("2023-11-20T19:00:00Z"), + }); + + // 1日後(削除対象外。本来はミリ秒単位の未来方向の時刻違いを想定)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + await makeTestTask(source, account02.id, admin02.id, "case07", { + status: TASK_STATUS.FINISHED, + job_number: "job07", + finished_at: new Date("2024-03-01T00:00:00Z"), + }); + + { + // ファイル削除対象のタスク情報6件のみを取得できる + const result = await getProcessTargets(source, now); + result.sort((a, b) => a.id - b.id); + expect(result).toEqual([ + { + id: task01.id, + audio_file_id: file01.id, + raw_file_name: file01.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task02.id, + audio_file_id: file02.id, + raw_file_name: file02.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task03.id, + audio_file_id: file03.id, + raw_file_name: file03.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task04.id, + audio_file_id: file04.id, + raw_file_name: file04.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + { + id: task05.id, + audio_file_id: file05.id, + raw_file_name: file05.raw_file_name, + account_id: account01.id, + country: account01.country, + }, + { + id: task06.id, + audio_file_id: file06.id, + raw_file_name: file06.raw_file_name, + account_id: account02.id, + country: account02.country, + }, + ]); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(20); + const files = await getAudioFiles(source); + expect(files.length).toEqual(20); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(40); + } + + // 削除対象のタスク情報を削除 + await deleteRecords(source, result); + + // 削除後のタスク情報を取得 + const result2 = await getProcessTargets(source, now); + // 削除対象のタスク情報が削除されているので取得が0件になる + expect(result2).toEqual([]); + + { + // DB全体のレコードを確認 + // 削除対象外のタスク情報のみが残っている + const tasks = await getTasks(source); + expect(tasks.length).toEqual(14); + const files = await getAudioFiles(source); + expect(files.length).toEqual(14); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(28); + } + } + }); + + it("対象としたタスクやAudioFile等が大量に存在しても削除される", async () => { + if (!source) fail(); + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + // "大量"の数を定義 + const count = 10000; + + // auto_file_deleteがtrueののアカウントを作成 + const { account, admin } = await makeTestAccount(source, { + file_retention_days: 30, + auto_file_delete: true, + }); + + // ファイルを10000件作成 + const createdFiles = [...Array(count).keys()].map( + (index): QueryDeepPartialEntity => { + return { + account_id: account.id, + owner_user_id: admin.id, + url: `https://example.com/${index}`, + file_name: `test${index}.wav`, + raw_file_name: `test${index}.wav`, + author_id: "test_author", + work_type_id: "test_work_type", + started_at: new Date(), + duration: "00:00:00", + finished_at: new Date(), + uploaded_at: new Date(), + file_size: 1024, + priority: "01", + audio_format: "wav", + comment: `test_comment_${index}`, + deleted_at: new Date(), + is_encrypted: false, + }; + } + ); + + // ファイルを元に、10年前に完了した扱いのタスクを作成 + const finished_at = new Date("2014-02-26T23:59:59Z"); + await makeManyTestTasks(source, createdFiles, finished_at); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(count); + + const files = await getAudioFiles(source); + expect(files.length).toEqual(count); + + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2 * count); + } + + const result = await getProcessTargets(source, now); + expect(result.length).toEqual(count); + + await deleteRecords(source, result); + + { + // 削除後のタスク情報を取得 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(0); + + const files = await getAudioFiles(source); + expect(files.length).toEqual(0); + + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(0); + } + }, 100000); +}); + +describe("deleteAudioFilesProcessing", () => { + let source: DataSource | null = null; + beforeAll(async () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: "mysql", + host: "test_mysql_db", + port: 3306, + username: "user", + password: "password", + database: "odms", + entities: [__dirname + "/../../**/*.entity{.ts,.js}"], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger("none"), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it("BlobとDBの削除が正常に行われる", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + const { account: account02, admin: admin02 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + country: "JP", + } + ); + + // ちょうど2日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case01", { + status: TASK_STATUS.FINISHED, + job_number: "job01", + finished_at: new Date("2024-02-27T00:00:00Z"), + }); + // ちょうど1日前(削除対象外)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case02", { + status: TASK_STATUS.FINISHED, + job_number: "job02", + finished_at: new Date("2024-02-28T00:00:00Z"), + }); + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file1 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file2 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case04", + { + status: TASK_STATUS.FINISHED, + job_number: "job04", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file3 } = await makeTestTask( + source, + account02.id, + admin02.id, + "case05", + { + status: TASK_STATUS.FINISHED, + job_number: "job05", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + args.push({ accountId, country, fileName }); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(5); + const files = await getAudioFiles(source); + expect(files.length).toEqual(5); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(10); + } + + const context = new InvocationContext(); + await deleteAudioFilesProcessing(context, source, blobstorage, now); + + // 想定通りの呼び出しが行われているか + { + const { accountId, country, fileName } = args[0]; + expect(fileName).toEqual(file1.raw_file_name); + expect(accountId).toEqual(account01.id); + expect(country).toEqual(account01.country); + } + { + const { accountId, country, fileName } = args[1]; + expect(fileName).toEqual(file2.raw_file_name); + expect(accountId).toEqual(account01.id); + expect(country).toEqual(account01.country); + } + { + const { accountId, country, fileName } = args[2]; + expect(fileName).toEqual(file3.raw_file_name); + expect(accountId).toEqual(account02.id); + expect(country).toEqual(account02.country); + } + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(2); + const files = await getAudioFiles(source); + expect(files.length).toEqual(2); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(4); + } + }); + + it("Blobの削除でエラーが発生した場合、アラーム発報用のログが出力される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + const { file: file1 } = await makeTestTask( + source, + account01.id, + admin01.id, + "case03", + { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + } + ); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + throw new Error( + `delete blob failed. succeeded: ${false}, errorCode: ${"ERROR_CODE"}, date: ${"DATE"}` + ); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(1); + const files = await getAudioFiles(source); + expect(files.length).toEqual(1); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2); + } + + const context = new TestInvocationContext(); + await deleteAudioFilesProcessing(context, source, blobstorage, now); + + const log = context + .getLogs() + .find((log) => log.includes(MANUAL_RECOVERY_REQUIRED)); + expect(log).toBeDefined(); + expect(log).toEqual( + `[MANUAL_RECOVERY_REQUIRED] file delete failed. target={"id":"1","audio_file_id":"1","account_id":"1","country":"US","raw_file_name":"testcase03.wav"}` + ); + }); + + it("DBの削除でエラーが発生した場合、アラーム発報用のログが出力される", async () => { + if (!source) fail(); + + // 2024/02/29 00:00:00を"今"とする + const now = new Date("2024-02-29T00:00:00Z"); + + const { account: account01, admin: admin01 } = await makeTestAccount( + source, + { + file_retention_days: 2, + auto_file_delete: true, + } + ); + + // 2日と1秒前(削除対象)のタスクを作成 + await makeTestTask(source, account01.id, admin01.id, "case03", { + status: TASK_STATUS.FINISHED, + job_number: "job03", + finished_at: new Date("2024-02-26T23:59:59Z"), + }); + + const args: { accountId: number; fileName: string; country: string }[] = []; + const blobstorage = new AudioBlobStorageService(); + Object.defineProperty(blobstorage, blobstorage.deleteFile.name, { + value: async ( + context: InvocationContext, + accountId: number, + country: string, + fileName: string + ): Promise => { + args.push({ accountId, country, fileName }); + }, + writable: true, + }); + + { + // DB全体のレコードを確認 + const tasks = await getTasks(source); + expect(tasks.length).toEqual(1); + const files = await getAudioFiles(source); + expect(files.length).toEqual(1); + const optionItems = await getAudioOptionItems(source); + expect(optionItems.length).toEqual(2); + } + + Object.defineProperty(source, "transaction", { + value: async () => { + throw new Error("transaction error"); + }, + writable: true, + }); + + const context = new TestInvocationContext(); + + await expect( + deleteAudioFilesProcessing(context, source, blobstorage, now) + ).rejects.toThrow(); + + const log = context + .getLogs() + .find((log) => log.includes(MANUAL_RECOVERY_REQUIRED)); + expect(log).toBeDefined(); + expect(log).toEqual( + `[MANUAL_RECOVERY_REQUIRED] Failed to execute auto file deletion function. error=Error: transaction error` + ); + }); +}); diff --git a/dictation_function/src/test/importUsers.spec.ts b/dictation_function/src/test/importUsers.spec.ts new file mode 100644 index 0000000..d35bb73 --- /dev/null +++ b/dictation_function/src/test/importUsers.spec.ts @@ -0,0 +1,90 @@ +import * as dotenv from "dotenv"; +import { InvocationContext } from "@azure/functions"; +import { BlobstorageService } from "../blobstorage/blobstorage.service"; +import { importUsersProcessing } from "../functions/importUsers"; +import { + PostMultipleImportsCompleteRequest, + SignupRequest, + UsersApi, +} from "../api/api"; +import { AxiosRequestConfig, AxiosResponse } from "axios"; + +describe("importUsersProcessing", () => { + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.test", override: true }); + + it("stage.jsonがない状態でユーザー追加できること", async () => { + const context = new InvocationContext(); + + const userApiMock = new UsersApiMock() as UsersApi; + + // // 呼び出し回数でテスト成否を判定 + const spySignup = jest.spyOn(userApiMock, "signup"); + + const blobService = new BlobstorageService(); + + const mockListBlobs = jest + .fn() + .mockReturnValueOnce(["U_20210101_000000.json"]) + .mockReturnValue([]); + const mockDownloadFileData = jest + .fn() + .mockReturnValueOnce(undefined) + .mockReturnValue( + `{"account_id": 1, "user_id": 1, "external_id": "hoge", "user_role": "none", "file_name": "U_20240216_143802_8001_12211.csv", "date": 1111111, "data": [{"name": "name1","email": "email1@example.com","role": 1,"author_id": "AUTHOR_ID1","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1},{"name": "name2","email": "email2@example.com","role": 1,"author_id": "AUTHOR_ID2","auto_renew": 1,"notification": 1,"encryption": 1,"encryption_password": "password","prompt": 1}]}` + ); + const mockUpdateFile = jest.fn().mockReturnValue(true); + const mockDeleteFile = jest.fn().mockReturnValue(undefined); + const mockIsFileExists = jest.fn().mockReturnValueOnce(false); + + blobService.listBlobs = mockListBlobs; + blobService.downloadFileData = mockDownloadFileData; + blobService.updateFile = mockUpdateFile; + blobService.deleteFile = mockDeleteFile; + blobService.isFileExists = mockIsFileExists; + + await importUsersProcessing(context, blobService, userApiMock); + expect(spySignup.mock.calls).toHaveLength(2); + }, 30000); + + it("ファイルがない場合はそのまま終了すること", async () => { + const context = new InvocationContext(); + + const userApiMock = new UsersApiMock() as UsersApi; + + // // 呼び出し回数でテスト成否を判定 + const spySignup = jest.spyOn(userApiMock, "signup"); + + const blobService = new BlobstorageService(); + + const mockListBlobs = jest.fn().mockReturnValue([]); + const mockDownloadFileData = jest.fn().mockReturnValue(""); + const mockUpdateFile = jest.fn().mockReturnValue(undefined); + const mockDeleteFile = jest.fn().mockReturnValue(undefined); + const mockIsFileExists = jest.fn().mockReturnValueOnce(false); + + blobService.listBlobs = mockListBlobs; + blobService.downloadFileData = mockDownloadFileData; + blobService.updateFile = mockUpdateFile; + blobService.deleteFile = mockDeleteFile; + blobService.isFileExists = mockIsFileExists; + + await importUsersProcessing(context, blobService, userApiMock); + expect(spySignup.mock.calls).toHaveLength(0); + }, 30000); +}); + +export class UsersApiMock extends UsersApi { + async signup( + ignupRequest: SignupRequest, + options?: AxiosRequestConfig + ): Promise> { + return { data: {}, status: 200, statusText: "", headers: {}, config: {} }; + } + async multipleImportsComplate( + postMultipleImportsCompleteRequest: PostMultipleImportsCompleteRequest, + options?: AxiosRequestConfig + ): Promise> { + return { data: {}, status: 200, statusText: "", headers: {}, config: {} }; + } +} diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts index 42cfc51..b30ba01 100644 --- a/dictation_function/src/test/licenseAlert.spec.ts +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -18,7 +18,7 @@ import { AdB2cServiceMock } from "./common/adb2c.mock"; describe("licenseAlert", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); let source: DataSource | null = null; const redisClient = createClient(); diff --git a/dictation_function/src/test/licenseAutoAllocation.spec.ts b/dictation_function/src/test/licenseAutoAllocation.spec.ts index 24919e4..5e2cced 100644 --- a/dictation_function/src/test/licenseAutoAllocation.spec.ts +++ b/dictation_function/src/test/licenseAutoAllocation.spec.ts @@ -23,7 +23,7 @@ import { createClient } from "redis-mock"; describe("licenseAutoAllocation", () => { dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); + dotenv.config({ path: ".env.test", override: true }); let source: DataSource | null = null; const redisClient = createClient(); beforeEach(async () => { diff --git a/dictation_server/.env b/dictation_server/.env index c104e29..e519213 100644 --- a/dictation_server/.env +++ b/dictation_server/.env @@ -1,5 +1,6 @@ DB_HOST=omds-mysql DB_PORT=3306 DB_NAME=omds +DB_NAME_CCB=omds_ccb DB_USERNAME=omdsdbuser DB_PASSWORD=omdsdbpass diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 5aaecca..31c4508 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -20,12 +20,15 @@ STORAGE_TOKEN_EXPIRE_TIME=2 STORAGE_ACCOUNT_NAME_US=saodmsusdev STORAGE_ACCOUNT_NAME_AU=saodmsaudev STORAGE_ACCOUNT_NAME_EU=saodmseudev +STORAGE_ACCOUNT_NAME_IMPORTS=saodmseudev STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXX +STORAGE_ACCOUNT_KEY_IMPORTS=XXXXXXXXXXXXXXXXXXXXXXX STORAGE_ACCOUNT_ENDPOINT_US=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_AU=https://AAAAAAAAAAAAA STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA +STORAGE_ACCOUNT_ENDPOINT_IMPORTS=https://AAAAAAAAAAAAA ACCESS_TOKEN_LIFETIME_WEB=7200 REFRESH_TOKEN_LIFETIME_WEB=86400 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000 diff --git a/dictation_server/.env.test b/dictation_server/.env.test index ca88821..c6730fb 100644 --- a/dictation_server/.env.test +++ b/dictation_server/.env.test @@ -1,4 +1,4 @@ -STAGE=local +STAGE=production NO_COLOR=TRUE CORS=TRUE PORT=8081 @@ -20,12 +20,15 @@ STORAGE_TOKEN_EXPIRE_TIME=30 STORAGE_ACCOUNT_NAME_US=saxxxxusxxx STORAGE_ACCOUNT_NAME_AU=saxxxxauxxx STORAGE_ACCOUNT_NAME_EU=saxxxxeuxxx +STORAGE_ACCOUNT_NAME_IMPORTS=saxxxximportsxxx STORAGE_ACCOUNT_KEY_US=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_KEY_AU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_KEY_EU=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== +STORAGE_ACCOUNT_KEY_IMPORTS=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX== STORAGE_ACCOUNT_ENDPOINT_US=https://xxxxxxxxxxxx.blob.core.windows.net/ STORAGE_ACCOUNT_ENDPOINT_AU=https://xxxxxxxxxxxx.blob.core.windows.net/ STORAGE_ACCOUNT_ENDPOINT_EU=https://xxxxxxxxxxxx.blob.core.windows.net/ +STORAGE_ACCOUNT_ENDPOINT_IMPORTS=https://xxxxxxxxxxxx.blob.core.windows.net/ ACCESS_TOKEN_LIFETIME_WEB=7200000 REFRESH_TOKEN_LIFETIME_WEB=86400000 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 diff --git a/dictation_server/.gitignore b/dictation_server/.gitignore index 63e9c25..21d6308 100644 --- a/dictation_server/.gitignore +++ b/dictation_server/.gitignore @@ -3,6 +3,7 @@ /dump.rdb /build /openapi/build +/.test # credentials credentials diff --git a/dictation_server/db/dbconfig.yml b/dictation_server/db/dbconfig.yml index f19fc0c..48f97aa 100644 --- a/dictation_server/db/dbconfig.yml +++ b/dictation_server/db/dbconfig.yml @@ -2,11 +2,19 @@ local: dialect: mysql dir: /app/dictation_server/db/migrations datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true +ccb: + dialect: mysql + dir: /app/dictation_server/db/migrations + datasource: ${DB_USERNAME}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME_CCB}?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true ci: dialect: mysql dir: ./dictation_server/db/migrations datasource: DB_USERNAME:DB_PASS@tcp(DB_HOST:DB_PORT)/DB_NAME?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true +ci_ccb: + dialect: mysql + dir: ./dictation_server/db/migrations + datasource: DB_USERNAME:DB_PASS@tcp(DB_HOST:DB_PORT)/DB_NAME_CCB?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true test: dialect: mysql dir: /app/dictation_server/db/migrations - datasource: user:password@tcp(test_mysql_db:3306)/odms?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true \ No newline at end of file + datasource: user:password@tcp(test_mysql_db:3306)/odms?charset=utf8mb4&collation=utf8mb4_0900_ai_ci&parseTime=true diff --git a/dictation_server/db/migrations/057-add_accounts_file-delete.sql b/dictation_server/db/migrations/057-add_accounts_file-delete.sql new file mode 100644 index 0000000..df96546 --- /dev/null +++ b/dictation_server/db/migrations/057-add_accounts_file-delete.sql @@ -0,0 +1,9 @@ +-- +migrate Up +ALTER TABLE `accounts` +ADD COLUMN `auto_file_delete` BOOLEAN NOT NULL DEFAULT 0 COMMENT '自動ファイル削除をするかどうか' AFTER `active_worktype_id`, +ADD COLUMN `file_retention_days` INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '文字起こし完了してから自動ファイル削除するまでのファイル保持日数' AFTER `auto_file_delete`; + +-- +migrate Down +ALTER TABLE `accounts` +DROP COLUMN `auto_file_delete`, +DROP COLUMN `file_retention_days`; \ No newline at end of file diff --git a/dictation_server/db/migrations/059-add_tasks_template_index.sql b/dictation_server/db/migrations/059-add_tasks_template_index.sql new file mode 100644 index 0000000..17a871c --- /dev/null +++ b/dictation_server/db/migrations/059-add_tasks_template_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `tasks` ADD INDEX `idx_tasks_template_file_id` (template_file_id); + +-- +migrate Down +ALTER TABLE `tasks` DROP INDEX `idx_tasks_template_file_id`; \ No newline at end of file diff --git a/dictation_server/db/migrations/060-create_accounts_archive.sql b/dictation_server/db/migrations/060-create_accounts_archive.sql new file mode 100644 index 0000000..e6e8f57 --- /dev/null +++ b/dictation_server/db/migrations/060-create_accounts_archive.sql @@ -0,0 +1,24 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS `accounts_archive` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ID', + `parent_account_id` BIGINT UNSIGNED COMMENT '親アカウントID', + `tier` INT UNSIGNED NOT NULL COMMENT '商流における階層', + `country` VARCHAR(16) NOT NULL COMMENT '国名(ISO 3166-1 alpha-2)', + `delegation_permission` BOOLEAN NOT NULL DEFAULT 0 COMMENT '上位階層からの代行操作を許可しているか', + `locked` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'アカウントがロック済みであるか', + `verified` BOOLEAN NOT NULL DEFAULT 0 COMMENT 'email認証が完了済みであるか', + `primary_admin_user_id` BIGINT UNSIGNED COMMENT 'プライマリ管理者ユーザーID', + `secondary_admin_user_id` BIGINT UNSIGNED COMMENT 'セカンダリ管理者ユーザーID', + `active_worktype_id` BIGINT UNSIGNED COMMENT 'アカウントで利用するデフォルトのWorkTypeID(Active WorktypeID)の内部ID', + `auto_file_delete` BOOLEAN NOT NULL DEFAULT 0 COMMENT '自動ファイル削除をするかどうか', + `file_retention_days` INT UNSIGNED COMMENT '文字起こし完了してから自動ファイル削除するまでのファイル保持日数', + `deleted_at` TIMESTAMP COMMENT '削除時刻', + `created_by` VARCHAR(255) COMMENT '作成者', + `created_at` TIMESTAMP DEFAULT now() COMMENT '作成時刻', + `updated_by` VARCHAR(255) COMMENT '更新者', + `updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻', + INDEX `idx_accounts_archive_tier` (`tier`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; + +-- +migrate Down +DROP TABLE `accounts_archive`; \ No newline at end of file diff --git a/dictation_server/db/migrations/061-add_license_orders_index.sql b/dictation_server/db/migrations/061-add_license_orders_index.sql new file mode 100644 index 0000000..98004c9 --- /dev/null +++ b/dictation_server/db/migrations/061-add_license_orders_index.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `license_orders` ADD INDEX `idx_from_account_id_and_status` (from_account_id,status); + +-- +migrate Down +ALTER TABLE `license_orders` DROP INDEX `idx_from_account_id_and_status`; diff --git a/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql b/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql new file mode 100644 index 0000000..015d4ec --- /dev/null +++ b/dictation_server/db/migrations/062-add_audio_file_raw_file_name.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) NOT NULL COMMENT '生ファイル名' AFTER `file_name`; + +-- +migrate Down +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; diff --git a/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql b/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql new file mode 100644 index 0000000..d7beef4 --- /dev/null +++ b/dictation_server/db/migrations/063-add_audio_file_raw_file_name_default.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) DEFAULT '' NOT NULL COMMENT '生ファイル名' AFTER `file_name`; + +-- +migrate Down +ALTER TABLE `audio_files` DROP COLUMN `raw_file_name`; +ALTER TABLE `audio_files` ADD COLUMN `raw_file_name` VARCHAR(1024) NOT NULL COMMENT '生ファイル名' AFTER `file_name`; \ No newline at end of file diff --git a/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql b/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql new file mode 100644 index 0000000..e08aff8 --- /dev/null +++ b/dictation_server/db/migrations/064-update_raw_file_name_from_file_name.sql @@ -0,0 +1,7 @@ +-- +migrate Up +UPDATE `audio_files` SET `raw_file_name` = `file_name`; +UPDATE `audio_files` SET `file_name` = TRIM(TRAILING '.zip' FROM `raw_file_name`); + +-- +migrate Down +UPDATE `audio_files` SET `file_name` = `raw_file_name`; +UPDATE `audio_files` SET `raw_file_name` = ''; \ No newline at end of file diff --git a/dictation_server/db/migrations/065-create_job_number.sql b/dictation_server/db/migrations/065-create_job_number.sql new file mode 100644 index 0000000..3ed6b7d --- /dev/null +++ b/dictation_server/db/migrations/065-create_job_number.sql @@ -0,0 +1,13 @@ +-- +migrate Up +CREATE TABLE IF NOT EXISTS `job_number` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY COMMENT 'ID', + `account_id` BIGINT UNSIGNED NOT NULL COMMENT 'アカウントID', + `job_number` VARCHAR(10) NOT NULL COMMENT 'JOBナンバー', + `updated_at` TIMESTAMP DEFAULT now() COMMENT '更新時刻', + CONSTRAINT `unique_account_id` UNIQUE (`account_id`), + CONSTRAINT `fk_account_id` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`), + INDEX `idx_account_id` (`account_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; + +-- +migrate Down +DROP TABLE `job_number`; \ No newline at end of file diff --git a/dictation_server/db/migrations/066-insert_initial_job_number.sql b/dictation_server/db/migrations/066-insert_initial_job_number.sql new file mode 100644 index 0000000..985026e --- /dev/null +++ b/dictation_server/db/migrations/066-insert_initial_job_number.sql @@ -0,0 +1,19 @@ +-- +migrate Up +INSERT INTO job_number (account_id, job_number) +SELECT + a.id AS account_id, + COALESCE(t.max_job_number, '00000000') AS job_number +FROM + accounts a +LEFT JOIN ( + SELECT + account_id, + MAX(job_number) AS max_job_number + FROM + tasks + GROUP BY + account_id +) t ON a.id = t.account_id; + +-- +migrate Down +TRUNCATE TABLE job_number; \ No newline at end of file diff --git a/dictation_server/package.json b/dictation_server/package.json index 7ca9f5b..a8d50bd 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -24,9 +24,9 @@ "test:e2e": "jest --config ./test/jest-e2e.json", "og": "openapi-generator-cli", "openapi-format": "cat \"src/api/odms/openapi.json\" | jq -c . > \"src/api/odms/openapi.json\" && prettier --write \"src/api/odms/*.json\"", - "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=local", - "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=local", - "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=local", + "migrate:up": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:down": "sql-migrate down -config=/app/dictation_server/db/dbconfig.yml -env=ccb", + "migrate:status": "sql-migrate status -config=/app/dictation_server/db/dbconfig.yml -env=ccb", "migrate:up:test": "sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test" }, "dependencies": { diff --git a/dictation_server/src/api/odms/openapi.json b/dictation_server/src/api/odms/openapi.json index e7a16ad..77983dc 100644 --- a/dictation_server/src/api/odms/openapi.json +++ b/dictation_server/src/api/odms/openapi.json @@ -651,6 +651,59 @@ "security": [{ "bearer": [] }] } }, + "/accounts/typist-groups/{typistGroupId}/delete": { + "post": { + "operationId": "deleteTypistGroup", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します", + "parameters": [ + { + "name": "typistGroupId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTypistGroupResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/partner": { "post": { "operationId": "createPartnerAccount", @@ -1364,6 +1417,61 @@ "security": [{ "bearer": [] }] } }, + "/accounts/me/file-delete-setting": { + "post": { + "operationId": "updateFileDeleteSetting", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateFileDeleteSettingResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/アカウント・ユーザー不在", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/accounts/delete": { "post": { "operationId": "deleteAccountAndData", @@ -1501,6 +1609,281 @@ "security": [{ "bearer": [] }] } }, + "/accounts/restriction-status": { + "post": { + "operationId": "updateRestrictionStatus", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRestrictionStatusResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/parent/switch": { + "post": { + "operationId": "switchParent", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SwitchParentRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchParentResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner/delete": { + "post": { + "operationId": "deletePartnerAccount", + "summary": "", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePartnerAccountRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePartnerAccountResponse" + } + } + } + }, + "400": { + "description": "実施者との親子関係不正や下位アカウント存在など削除実施条件に合致しない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner/users": { + "post": { + "operationId": "getPartnerUsers", + "summary": "", + "description": "パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用)", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerUsersRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPartnerUsersResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/API実行者と取得対象が親子関係ではない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, + "/accounts/partner/update": { + "post": { + "operationId": "updatePartnerInfo", + "summary": "", + "description": "パートナーアカウントの情報を更新します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerInfoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerInfoResponse" + } + } + } + }, + "400": { + "description": "パラメータ不正/API実行者と取得対象が親子関係ではない/アカウントが不在/プライマリ管理者が同一アカウント内にいない", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["accounts"], + "security": [{ "bearer": [] }] + } + }, "/users/confirm": { "post": { "operationId": "confirmUser", @@ -2050,6 +2433,172 @@ "security": [{ "bearer": [] }] } }, + "/users/delete": { + "post": { + "operationId": "deleteUser", + "summary": "", + "description": "ユーザーを削除します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PostDeleteUserRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteUserResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports": { + "post": { + "operationId": "multipleImports", + "summary": "", + "description": "ユーザーを一括登録します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, + "/users/multiple-imports/complete": { + "post": { + "operationId": "multipleImportsComplate", + "summary": "", + "description": "ユーザー一括登録の完了を通知します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteRequest" + } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostMultipleImportsCompleteResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["users"], + "security": [{ "bearer": [] }] + } + }, "/files/audio/upload-finished": { "post": { "operationId": "uploadFinished", @@ -2354,6 +2903,58 @@ "security": [{ "bearer": [] }] } }, + "/files/rename": { + "post": { + "operationId": "fileRename", + "summary": "", + "description": "音声ファイルの表示ファイル名を変更します", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameRequest" } + } + } + }, + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/FileRenameResponse" } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["files"], + "security": [{ "bearer": [] }] + } + }, "/tasks": { "get": { "operationId": "getTasks", @@ -2869,6 +3470,60 @@ "security": [{ "bearer": [] }] } }, + "/tasks/{audioFileId}/delete": { + "post": { + "operationId": "deleteTask", + "summary": "", + "description": "指定した文字起こしタスクを削除します。", + "parameters": [ + { + "name": "audioFileId", + "required": true, + "in": "path", + "description": "ODMS Cloud上の音声ファイルID", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostDeleteTaskResponse" + } + } + } + }, + "400": { + "description": "不正なパラメータ", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["tasks"], + "security": [{ "bearer": [] }] + } + }, "/licenses/orders": { "post": { "operationId": "createOrders", @@ -3152,6 +3807,59 @@ "security": [{ "bearer": [] }] } }, + "/templates/{templateFileId}/delete": { + "post": { + "operationId": "deleteTemplateFile", + "summary": "", + "description": "ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します", + "parameters": [ + { + "name": "templateFileId", + "required": true, + "in": "path", + "schema": { "type": "number" } + } + ], + "responses": { + "200": { + "description": "成功時のレスポンス", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTemplateResponse" + } + } + } + }, + "400": { + "description": "ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "401": { + "description": "認証エラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + }, + "500": { + "description": "想定外のサーバーエラー", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ErrorResponse" } + } + } + } + }, + "tags": ["templates"], + "security": [{ "bearer": [] }] + } + }, "/workflows": { "get": { "operationId": "getWorkflows", @@ -3603,6 +4311,8 @@ "country": { "type": "string" }, "parentAccountId": { "type": "number" }, "delegationPermission": { "type": "boolean" }, + "autoFileDelete": { "type": "boolean" }, + "fileRetentionDays": { "type": "number" }, "primaryAdminUserId": { "type": "number" }, "secondryAdminUserId": { "type": "number" }, "parentAccountName": { "type": "string" } @@ -3612,7 +4322,9 @@ "companyName", "tier", "country", - "delegationPermission" + "delegationPermission", + "autoFileDelete", + "fileRetentionDays" ] }, "GetMyAccountResponse": { @@ -3715,6 +4427,7 @@ }, "required": ["typistGroupName", "typistIds"] }, + "DeleteTypistGroupResponse": { "type": "object", "properties": {} }, "CreatePartnerAccountRequest": { "type": "object", "properties": { @@ -4041,6 +4754,21 @@ "required": ["delegationPermission", "primaryAdminUserId"] }, "UpdateAccountInfoResponse": { "type": "object", "properties": {} }, + "UpdateFileDeleteSettingRequest": { + "type": "object", + "properties": { + "autoFileDelete": { + "type": "boolean", + "description": "自動ファイル削除をするかどうか" + }, + "retentionDays": { + "type": "number", + "description": "文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間" + } + }, + "required": ["autoFileDelete", "retentionDays"] + }, + "UpdateFileDeleteSettingResponse": { "type": "object", "properties": {} }, "DeleteAccountRequest": { "type": "object", "properties": { @@ -4070,6 +4798,98 @@ "properties": { "companyName": { "type": "string" } }, "required": ["companyName"] }, + "UpdateRestrictionStatusRequest": { + "type": "object", + "properties": { + "accountId": { + "type": "number", + "description": "操作対象の第五階層アカウントID" + }, + "restricted": { + "type": "boolean", + "description": "制限をかけるかどうか(trur:制限をかける)" + } + }, + "required": ["accountId", "restricted"] + }, + "UpdateRestrictionStatusResponse": { "type": "object", "properties": {} }, + "SwitchParentRequest": { + "type": "object", + "properties": { + "to": { + "type": "number", + "description": "切り替え先の親アカウントID" + }, + "children": { + "minItems": 1, + "description": "親を変更したいアカウントIDのリスト", + "type": "array", + "items": { "type": "integer" } + } + }, + "required": ["to", "children"] + }, + "SwitchParentResponse": { "type": "object", "properties": {} }, + "DeletePartnerAccountRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "削除対象のアカウントID" + } + }, + "required": ["targetAccountId"] + }, + "DeletePartnerAccountResponse": { "type": "object", "properties": {} }, + "GetPartnerUsersRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "取得対象のアカウントID" + } + }, + "required": ["targetAccountId"] + }, + "PartnerUser": { + "type": "object", + "properties": { + "id": { "type": "number", "description": "ユーザーID" }, + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "isPrimaryAdmin": { + "type": "boolean", + "description": "プライマリ管理者かどうか" + } + }, + "required": ["id", "name", "email", "isPrimaryAdmin"] + }, + "GetPartnerUsersResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/PartnerUser" } + } + }, + "required": ["users"] + }, + "UpdatePartnerInfoRequest": { + "type": "object", + "properties": { + "targetAccountId": { + "type": "number", + "description": "変更対象アカウントID" + }, + "primaryAdminUserId": { + "type": "number", + "description": "プライマリ管理者ID" + }, + "companyName": { "type": "string", "description": "会社名" } + }, + "required": ["targetAccountId", "primaryAdminUserId", "companyName"] + }, + "UpdatePartnerInfoResponse": { "type": "object", "properties": {} }, "ConfirmRequest": { "type": "object", "properties": { "token": { "type": "string" } }, @@ -4309,6 +5129,76 @@ }, "required": ["userName"] }, + "PostDeleteUserRequest": { + "type": "object", + "properties": { + "userId": { "type": "number", "description": "削除対象のユーザーID" } + }, + "required": ["userId"] + }, + "PostDeleteUserResponse": { "type": "object", "properties": {} }, + "MultipleImportUser": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "email": { "type": "string", "description": "メールアドレス" }, + "role": { + "type": "number", + "description": "0(none)/1(author)/2(typist)" + }, + "authorId": { "type": "string" }, + "autoRenew": { "type": "number", "description": "0(false)/1(true)" }, + "notification": { + "type": "number", + "description": "0(false)/1(true)" + }, + "encryption": { "type": "number", "description": "0(false)/1(true)" }, + "encryptionPassword": { "type": "string" }, + "prompt": { "type": "number", "description": "0(false)/1(true)" } + }, + "required": ["name", "email", "role", "autoRenew", "notification"] + }, + "PostMultipleImportsRequest": { + "type": "object", + "properties": { + "filename": { "type": "string", "description": "CSVファイル名" }, + "users": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportUser" } + } + }, + "required": ["filename", "users"] + }, + "PostMultipleImportsResponse": { "type": "object", "properties": {} }, + "MultipleImportErrors": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "ユーザー名" }, + "line": { "type": "number", "description": "エラー発生行数" }, + "errorCode": { "type": "string", "description": "エラーコード" } + }, + "required": ["name", "line", "errorCode"] + }, + "PostMultipleImportsCompleteRequest": { + "type": "object", + "properties": { + "accountId": { "type": "number", "description": "アカウントID" }, + "filename": { "type": "string", "description": "CSVファイル名" }, + "requestTime": { + "type": "number", + "description": "一括登録受付時刻(UNIXTIME/ミリ秒)" + }, + "errors": { + "type": "array", + "items": { "$ref": "#/components/schemas/MultipleImportErrors" } + } + }, + "required": ["accountId", "filename", "requestTime", "errors"] + }, + "PostMultipleImportsCompleteResponse": { + "type": "object", + "properties": {} + }, "AudioOptionItem": { "type": "object", "properties": { @@ -4445,6 +5335,18 @@ "required": ["name", "url"] }, "TemplateUploadFinishedReqponse": { "type": "object", "properties": {} }, + "FileRenameRequest": { + "type": "object", + "properties": { + "audioFileId": { + "type": "number", + "description": "ファイル名変更対象の音声ファイルID" + }, + "fileName": { "type": "string", "description": "変更するファイル名" } + }, + "required": ["audioFileId", "fileName"] + }, + "FileRenameResponse": { "type": "object", "properties": {} }, "Assignee": { "type": "object", "properties": { @@ -4484,6 +5386,10 @@ "description": "音声ファイルのBlob Storage上での保存場所(ファイル名含む)のURL" }, "fileName": { "type": "string", "description": "音声ファイル名" }, + "rawFileName": { + "type": "string", + "description": "生(Blob Storage上の)音声ファイル名" + }, "audioDuration": { "type": "string", "description": "音声ファイルの録音時間(ミリ秒の整数値)" @@ -4544,6 +5450,7 @@ "optionItemList", "url", "fileName", + "rawFileName", "audioDuration", "audioCreatedDate", "audioFinishedDate", @@ -4600,6 +5507,7 @@ "required": ["assignees"] }, "PostCheckoutPermissionResponse": { "type": "object", "properties": {} }, + "PostDeleteTaskResponse": { "type": "object", "properties": {} }, "CreateOrdersRequest": { "type": "object", "properties": { @@ -4673,6 +5581,7 @@ }, "required": ["templates"] }, + "DeleteTemplateResponse": { "type": "object", "properties": {} }, "WorkflowWorktype": { "type": "object", "properties": { diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index 1672c4e..cdc19b7 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -52,7 +52,9 @@ import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.re import { TermsModule } from './features/terms/terms.module'; import { RedisModule } from './gateways/redis/redis.module'; import * as redisStore from 'cache-manager-redis-store'; +import { SystemAccessGuardsModule } from './common/guards/system/accessguards.module'; import { CheckHeaderMiddleware } from './common/check-header.middleware'; +import { JobNumberRepositoryModule } from './repositories/job_number/job_number.repository.module'; @Module({ imports: [ ServeStaticModule.forRootAsync({ @@ -99,7 +101,7 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; port: configService.get('DB_PORT'), username: configService.get('DB_USERNAME'), password: configService.get('DB_PASSWORD'), - database: configService.get('DB_NAME'), + database: configService.get('DB_NAME_CCB'), autoLoadEntities: true, // forFeature()で登録されたEntityを自動的にロード synchronize: false, // trueにすると自動的にmigrationが行われるため注意 }), @@ -134,10 +136,12 @@ import { CheckHeaderMiddleware } from './common/check-header.middleware'; NotificationhubModule, BlobstorageModule, AuthGuardsModule, + SystemAccessGuardsModule, SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsModule, RedisModule, + JobNumberRepositoryModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/common/error/code.ts b/dictation_server/src/common/error/code.ts index 3c488da..740e842 100644 --- a/dictation_server/src/common/error/code.ts +++ b/dictation_server/src/common/error/code.ts @@ -67,4 +67,27 @@ export const ErrorCodes = [ 'E012001', // テンプレートファイル不在エラー 'E013001', // ワークフローのAuthorIDとWorktypeIDのペア重複エラー 'E013002', // ワークフロー不在エラー + 'E014001', // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった) + 'E014002', // ユーザー削除エラー(削除しようとしたユーザーが管理者だった) + 'E014003', // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた) + 'E014004', // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた) + 'E014005', // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた) + 'E014006', // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている) + 'E014007', // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた) + 'E014008', // ユーザー削除エラー(削除しようとしたユーザーが自分自身だった) + 'E014009', // ユーザー削除エラー(削除しようとしたユーザーがタスクのルーティング(文字起こし候補)になっている場合) + 'E015001', // タイピストグループ削除エラー(削除しようとしたタイピストグループがすでに削除済みだった) + 'E015002', // タイピストグループ削除エラー(削除しようとしたタイピストグループがWorkflowのTypist候補として指定されていた) + 'E015003', // タイピストグループ削除エラー(削除しようとしたタイピストグループがチェックアウト可能なタスクが存在した) + 'E016001', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった) + 'E016002', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた) + 'E016003', // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた) + 'E017001', // 親アカウント変更不可エラー(指定したアカウントが存在しない) + 'E017002', // 親アカウント変更不可エラー(階層関係が不正) + 'E017003', // 親アカウント変更不可エラー(リージョンが同一でない) + 'E018001', // パートナーアカウント削除エラー(削除条件を満たしていない) + 'E019001', // パートナーアカウント取得不可エラー(階層構造が不正) + 'E020001', // パートナーアカウント変更エラー(変更条件を満たしていない) + 'E021001', // 音声ファイル名変更不可エラー(権限不足) + 'E021002', // 音声ファイル名変更不可エラー(同名ファイルが存在) ] as const; diff --git a/dictation_server/src/common/error/message.ts b/dictation_server/src/common/error/message.ts index 9383694..89e51c0 100644 --- a/dictation_server/src/common/error/message.ts +++ b/dictation_server/src/common/error/message.ts @@ -56,4 +56,28 @@ export const errors: Errors = { E012001: 'Template file not found Error', E013001: 'AuthorId and WorktypeId pair already exists Error', E013002: 'Workflow not found Error', + E014001: 'User delete failed Error: already deleted', + E014002: 'User delete failed Error: target is admin', + E014003: 'User delete failed Error: workflow assigned(AUTHOR_ID)', + E014004: 'User delete failed Error: workflow assigned(TYPIST)', + E014005: 'User delete failed Error: typist group assigned', + E014006: 'User delete failed Error: checkout permission existed', + E014007: 'User delete failed Error: enabled license assigned', + E014008: 'User delete failed Error: delete myself', + E014009: 'User delete failed Error: user has checkout permissions.', + E015001: 'Typist Group delete failed Error: already deleted', + E015002: 'Typist Group delete failed Error: workflow assigned', + E015003: 'Typist Group delete failed Error: checkout permission existed', + E016001: 'Template file delete failed Error: already deleted', + E016002: 'Template file delete failed Error: workflow assigned', + E016003: + 'Template file delete failed Error: not finished task has template file', + E017001: 'Parent account switch failed Error: account not found', + E017002: 'Parent account switch failed Error: hierarchy mismatch', + E017003: 'Parent account switch failed Error: region mismatch', + E018001: 'Partner account delete failed Error: not satisfied conditions', + E019001: 'Partner account get failed Error: hierarchy mismatch', + E020001: 'Partner account change failed Error: not satisfied conditions', + E021001: 'Audio file name change failed Error: insufficient permissions', + E021002: 'Audio file name change failed Error: same file name exists', }; diff --git a/dictation_server/src/common/guards/system/accessguards.module.ts b/dictation_server/src/common/guards/system/accessguards.module.ts new file mode 100644 index 0000000..43ac336 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { SystemAccessGuard } from './accessguards'; + +@Module({ + imports: [ConfigModule], + controllers: [], + providers: [SystemAccessGuard], +}) +export class SystemAccessGuardsModule {} diff --git a/dictation_server/src/common/guards/system/accessguards.ts b/dictation_server/src/common/guards/system/accessguards.ts new file mode 100644 index 0000000..4aa11e2 --- /dev/null +++ b/dictation_server/src/common/guards/system/accessguards.ts @@ -0,0 +1,43 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { isVerifyError, decode, verify } from '../../jwt'; +import { Request } from 'express'; +import { retrieveAuthorizationToken } from '../../../common/http/helper'; +import { makeErrorResponse } from '../../../common/error/makeErrorResponse'; +import { SystemAccessToken } from '../../token/types'; +import { ConfigService } from '@nestjs/config'; +import { getPublicKey } from '../../jwt/jwt'; +/** + * システム間通信用のトークンを検証するガード + **/ +@Injectable() +export class SystemAccessGuard implements CanActivate { + constructor(private readonly configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean | Promise { + const pubkey = getPublicKey(this.configService); + const req = context.switchToHttp().getRequest(); + + const token = retrieveAuthorizationToken(req); + if (!token) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const payload = verify(token, pubkey); + if (isVerifyError(payload)) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + return true; + } +} diff --git a/dictation_server/src/common/test/logger.ts b/dictation_server/src/common/test/logger.ts new file mode 100644 index 0000000..93067ff --- /dev/null +++ b/dictation_server/src/common/test/logger.ts @@ -0,0 +1,129 @@ +import { Logger, QueryRunner } from 'typeorm'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface IOutput { + initialize(): void; + write(message: string): void; +} + +class ConsoleOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + console.log(message); + } +} + +class FileOutput implements IOutput { + private logPath = path.join('/app/dictation_server/.test', 'logs'); + private fileName = new Date().getTime(); + + initialize(): void { + if (!fs.existsSync(this.logPath)) { + fs.mkdirSync(this.logPath, { recursive: true }); + } + } + + write(message: string): void { + const logFile = path.join(this.logPath, `${this.fileName}.log`); + fs.appendFileSync(logFile, `${message}\n`); + } +} + +class NoneOutput implements IOutput { + initialize(): void { + // do nothing + } + + write(message: string): void { + // do nothing + } +} + +export class TestLogger implements Logger { + out: IOutput; + + constructor(output: 'none' | 'file' | 'console') { + switch (output) { + case 'none': + this.out = new NoneOutput(); + break; + case 'file': + this.out = new FileOutput(); + break; + case 'console': + this.out = new ConsoleOutput(); + break; + default: + this.out = new NoneOutput(); + break; + } + this.out.initialize(); + } + + private write(message: string): void { + this.out.write(message); + } + + logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) { + const raw = `Query: ${query} -- Parameters: ${JSON.stringify(parameters)}`; + // ex: 2024-03-08T06:38:43.125Z を TIME という文字列に置換 + const dateRemoved = raw.replace( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/g, + 'TIME', + ); + // ex: /* コメント内容 */ を /* コメント */ という文字列に置換 + const commentRemoved = dateRemoved.replace( + /\/\*.*\*\//g, + '/* RequestID */', + ); + + // UUIDを固定文字列に置換する ex: 88a9c78e-115a-439c-9e23-731d649f0c27 を XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX という文字列に置換 + const uuidRemoved = commentRemoved.replace( + /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, + 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', + ); + this.write(uuidRemoved); + } + + logQueryError( + error: string, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.write( + `ERROR: ${error} -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logQuerySlow( + time: number, + query: string, + parameters?: any[], + queryRunner?: QueryRunner, + ) { + this.write( + `SLOW QUERY: ${time}ms -- Query: ${query} -- Parameters: ${JSON.stringify( + parameters, + )}`, + ); + } + + logSchemaBuild(message: string, queryRunner?: QueryRunner) { + this.write(`Schema Build: ${message}`); + } + + logMigration(message: string, queryRunner?: QueryRunner) { + this.write(`Migration: ${message}`); + } + + log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) { + this.write(`${level.toUpperCase()}: ${message}`); + } +} diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index 8d50139..149c8de 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -40,6 +40,7 @@ import { TermsModule } from '../../features/terms/terms.module'; import { CacheModule } from '@nestjs/common'; import { RedisModule } from '../../gateways/redis/redis.module'; import { RedisService } from '../../gateways/redis/redis.service'; +import { JobNumberRepositoryModule } from '../../repositories/job_number/job_number.repository.module'; export const makeTestingModule = async ( datasource: DataSource, @@ -79,6 +80,7 @@ export const makeTestingModule = async ( SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsRepositoryModule, + JobNumberRepositoryModule, RedisModule, CacheModule.register({ isGlobal: true, ttl: 86400 }), ], diff --git a/dictation_server/src/common/test/overrides.ts b/dictation_server/src/common/test/overrides.ts index ffaf05d..07e2025 100644 --- a/dictation_server/src/common/test/overrides.ts +++ b/dictation_server/src/common/test/overrides.ts @@ -35,6 +35,11 @@ export const overrideAdB2cService = ( externalIds: string[], ) => Promise; getUser?: (context: Context, externalId: string) => Promise; + changePassword?: ( + context: Context, + externalId: string, + password: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -69,6 +74,12 @@ export const overrideAdB2cService = ( writable: true, }); } + if (overrides.changePassword) { + Object.defineProperty(obj, obj.changePassword.name, { + value: overrides.changePassword, + writable: true, + }); + } }; /** @@ -122,6 +133,8 @@ export const overrideUsersRepositoryService = ( overrides: { createNormalUser?: (user: newUser) => Promise; deleteNormalUser?: (userId: number) => Promise; + updateUserVerified?: (context: Context, userId: number) => Promise; + updateUserUnverified?: (context: Context, userId: number) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -138,6 +151,18 @@ export const overrideUsersRepositoryService = ( writable: true, }); } + if (overrides.updateUserVerified) { + Object.defineProperty(obj, obj.updateUserVerified.name, { + value: overrides.updateUserVerified, + writable: true, + }); + } + if (overrides.updateUserUnverified) { + Object.defineProperty(obj, obj.updateUserUnverified.name, { + value: overrides.updateUserUnverified, + writable: true, + }); + } }; /** @@ -159,6 +184,12 @@ export const overrideBlobstorageService = ( accountId: number, country: string, ) => Promise; + deleteFile?: ( + context: Context, + accountId: number, + country: string, + fileName: string, + ) => Promise; containerExists?: ( context: Context, accountId: number, @@ -174,6 +205,11 @@ export const overrideBlobstorageService = ( accountId: number, country: string, ) => Promise; + uploadImportsBlob?: ( + context: Context, + fileName: string, + content: string, + ) => Promise; }, ): void => { // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得 @@ -190,6 +226,12 @@ export const overrideBlobstorageService = ( writable: true, }); } + if (overrides.deleteFile) { + Object.defineProperty(obj, obj.deleteFile.name, { + value: overrides.deleteFile, + writable: true, + }); + } if (overrides.containerExists) { Object.defineProperty(obj, obj.containerExists.name, { value: overrides.containerExists, @@ -208,6 +250,12 @@ export const overrideBlobstorageService = ( writable: true, }); } + if (overrides.uploadImportsBlob) { + Object.defineProperty(obj, obj.uploadImportsBlob.name, { + value: overrides.uploadImportsBlob, + writable: true, + }); + } }; /** diff --git a/dictation_server/src/common/test/utility.ts b/dictation_server/src/common/test/utility.ts index 60a6a41..2d531a4 100644 --- a/dictation_server/src/common/test/utility.ts +++ b/dictation_server/src/common/test/utility.ts @@ -2,8 +2,16 @@ import { v4 as uuidv4 } from 'uuid'; import { DataSource } from 'typeorm'; import { User, UserArchive } from '../../repositories/users/entity/user.entity'; import { Account } from '../../repositories/accounts/entity/account.entity'; -import { ADMIN_ROLES, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + FILE_RETENTION_DAYS_DEFAULT, + USER_ROLES, +} from '../../constants'; import { License } from '../../repositories/licenses/entity/license.entity'; +import { AccountArchive } from '../../repositories/accounts/entity/account_archive.entity'; +import { Task } from '../../repositories/tasks/entity/task.entity'; +import { JobNumber } from '../../repositories/job_number/entity/job_number.entity'; +import { SortCriteria } from '../../repositories/sort_criteria/entity/sort_criteria.entity'; type InitialTestDBState = { tier1Accounts: { account: Account; users: User[] }[]; @@ -162,6 +170,9 @@ export const makeTestAccount = async ( parent_account_id: d?.parent_account_id ?? undefined, country: d?.country ?? 'US', delegation_permission: d?.delegation_permission ?? false, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: + d?.file_retention_days ?? FILE_RETENTION_DAYS_DEFAULT, locked: d?.locked ?? false, company_name: d?.company_name ?? 'test inc.', verified: d?.verified ?? true, @@ -229,6 +240,11 @@ export const makeTestAccount = async ( if (!account || !admin) { throw new Error('Unexpected null'); } + // sort_criteriaテーブルにデータを追加 + await createSortCriteria(datasource, userId, 'JOB_NUMBER', 'ASC'); + + // job_numberテーブルにデータを追加 + await createJobNumber(datasource, accountId, '00000000'); return { account: account, @@ -252,6 +268,8 @@ export const makeTestSimpleAccount = async ( parent_account_id: d?.parent_account_id ?? undefined, country: d?.country ?? 'US', delegation_permission: d?.delegation_permission ?? false, + auto_file_delete: d?.auto_file_delete ?? false, + file_retention_days: d?.file_retention_days ?? FILE_RETENTION_DAYS_DEFAULT, locked: d?.locked ?? false, company_name: d?.company_name ?? 'test inc.', verified: d?.verified ?? true, @@ -313,6 +331,8 @@ export const makeTestUser = async ( if (!user) { throw new Error('Unexpected null'); } + // sort_criteriaテーブルにデータを追加 + await createSortCriteria(datasource, user.id, 'FILE_LENGTH', 'ASC'); return user; }; @@ -389,6 +409,12 @@ export const getUsers = async (dataSource: DataSource): Promise => { * @param dataSource データソース * @returns ユーザー退避テーブルの内容 */ +export const getAccountArchive = async ( + dataSource: DataSource, +): Promise => { + return await dataSource.getRepository(AccountArchive).find(); +}; + export const getUserArchive = async ( dataSource: DataSource, ): Promise => { @@ -405,3 +431,95 @@ export const getLicenses = async ( }); return licenses; }; + +export const getTasks = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const tasks = await datasource.getRepository(Task).find({ + where: { account_id: account_id }, + }); + return tasks; +}; + +// job_numberテーブルにレコードを作成する +export const createJobNumber = async ( + datasource: DataSource, + accountId: number, + jobNumber: string, +): Promise => { + await datasource.getRepository(JobNumber).insert({ + account_id: accountId, + job_number: jobNumber, + }); +}; + +// job_numberテーブルのレコードを更新する +export const updateJobNumber = async ( + datasource: DataSource, + accountId: number, + jobNumber: string, +): Promise => { + await datasource.getRepository(JobNumber).update( + { account_id: accountId }, + { + job_number: jobNumber, + }, + ); +}; + +// job_numberを取得する +export const getJobNumber = async ( + datasource: DataSource, + account_id: number, +): Promise => { + const jobNumber = await datasource.getRepository(JobNumber).findOne({ + where: { + account_id: account_id, + }, + }); + return jobNumber; +}; + +// sort_criteriaを作成する +export const createSortCriteria = async ( + datasource: DataSource, + userId: number, + parameter: string, + direction: string, +): Promise => { + await datasource.getRepository(SortCriteria).insert({ + user_id: userId, + parameter: parameter, + direction: direction, + }); +}; + +// 指定したユーザーのsort_criteriaを更新する +export const updateSortCriteria = async ( + datasource: DataSource, + userId: number, + parameter: string, + direction: string, +): Promise => { + await datasource.getRepository(SortCriteria).update( + { user_id: userId }, + { + parameter: parameter, + direction: direction, + }, + ); +}; + +// 指定したユーザーのsort_criteriaを取得する +export const getSortCriteria = async ( + datasource: DataSource, + userId: number, +): Promise => { + const sortCriteria = await datasource.getRepository(SortCriteria).findOne({ + where: { + user_id: userId, + }, + }); + return sortCriteria; +}; diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index b602913..02f746d 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -36,6 +36,20 @@ export type AccessToken = { tier: number; }; +// システムの内部で発行し、外部に公開しないトークン +// システム間通信用(例: Azure Functions→AppService)に使用する +export type SystemAccessToken = { + /** + * トークンの発行者名(ログ記録用) + */ + systemName: string; + + /** + * 付加情報を 文字情報として格納できる + */ + context?: string; +}; + export type IDToken = { emails: string[]; nonce?: string | undefined; diff --git a/dictation_server/src/common/validators/authorId.validator.ts b/dictation_server/src/common/validators/authorId.validator.ts index a790edc..de721b9 100644 --- a/dictation_server/src/common/validators/authorId.validator.ts +++ b/dictation_server/src/common/validators/authorId.validator.ts @@ -12,8 +12,8 @@ import { import { USER_ROLES } from '../../constants'; // 大文字英数字とアンダースコアのみを許可するバリデータ -@ValidatorConstraint({ name: 'IsAuthorId', async: false }) -export class IsAuthorId implements ValidatorConstraintInterface { +@ValidatorConstraint({ name: 'IsAuthorIdValidConstraint', async: false }) +export class IsAuthorIdValidConstraint implements ValidatorConstraintInterface { validate(value: any, args: ValidationArguments) { const request = args.object as SignupRequest | PostUpdateUserRequest; // requestの存在チェック @@ -40,12 +40,44 @@ export class IsAuthorId implements ValidatorConstraintInterface { export function IsAuthorIdValid(validationOptions?: ValidationOptions) { return function (object: object, propertyName: string) { registerDecorator({ - name: 'IsAuthorId', + name: 'IsAuthorIdValidConstraint', target: object.constructor, propertyName: propertyName, constraints: [], options: validationOptions, - validator: IsAuthorId, + validator: IsAuthorIdValidConstraint, + }); + }; +} + +@ValidatorConstraint({ async: false }) +class IsAuhtorIDConstraint implements ValidatorConstraintInterface { + validate(value: any, args: ValidationArguments) { + // null or undefinedであれば不合格 + if (value == null) { + return false; + } + // 文字列型でなければ不合格 + if (typeof value !== 'string') { + return false; + } + + return /^[A-Z0-9_]*$/.test(value); + } + + defaultMessage(args: ValidationArguments) { + return `${args.property} should be uppercase alphanumeric and underscore only`; + } +} + +export function IsAuthorID(validationOptions?: ValidationOptions) { + return function (object: Object, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsAuhtorIDConstraint, }); }; } diff --git a/dictation_server/src/common/validators/env.validator.ts b/dictation_server/src/common/validators/env.validator.ts index 8ce1547..a676908 100644 --- a/dictation_server/src/common/validators/env.validator.ts +++ b/dictation_server/src/common/validators/env.validator.ts @@ -24,6 +24,10 @@ export class EnvValidator { @IsString() DB_NAME: string; + @IsNotEmpty() + @IsString() + DB_NAME_CCB: string; + @IsNotEmpty() @IsString() DB_USERNAME: string; diff --git a/dictation_server/src/constants/index.ts b/dictation_server/src/constants/index.ts index 6657210..f295ed5 100644 --- a/dictation_server/src/constants/index.ts +++ b/dictation_server/src/constants/index.ts @@ -321,3 +321,33 @@ export const USER_LICENSE_STATUS = { ALLOCATED: 'allocated', EXPIRED: 'expired', } as const; + +/** + * ファイル保持日数の初期値 + * @const {number} + */ +export const FILE_RETENTION_DAYS_DEFAULT = 30; + +/** + * 割り当て履歴有りライセンス1つあたりのストレージ使用可能量(GB) + * @const {number} + */ +export const STORAGE_SIZE_PER_LICENSE = 5; + +/** + * ストレージ使用量の警告閾値(%) + * @const {number} + */ +export const STORAGE_WARNING_THRESHOLD_PERCENT = 80; + +/** + * JobNumberの初期値 + * @const {string} + */ +export const INITIAL_JOB_NUMBER = '00000000'; + +/** + * JobNumberの最大値 + * @const {string} + */ +export const MAX_JOB_NUMBER = '99999999'; diff --git a/dictation_server/src/features/accounts/accounts.controller.spec.ts b/dictation_server/src/features/accounts/accounts.controller.spec.ts index f69ece1..24bf667 100644 --- a/dictation_server/src/features/accounts/accounts.controller.spec.ts +++ b/dictation_server/src/features/accounts/accounts.controller.spec.ts @@ -3,6 +3,17 @@ import { AccountsController } from './accounts.controller'; import { AccountsService } from './accounts.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { + SwitchParentRequest, + DeletePartnerAccountRequest, + GetPartnerUsersRequest, + UpdatePartnerInfoRequest, + CreateWorktypesRequest, + UpdateWorktypesRequest, + UpdateWorktypeRequestParam, +} from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('AccountsController', () => { let controller: AccountsController; @@ -32,4 +43,418 @@ describe('AccountsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation CreateWorktypesRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdが指定されていない場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが空文字の場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = ''; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字を超える場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = '123456789A1234567'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字の場合、リクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = '123456789A123456'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdに使用不可文字が含まれる場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'test.test'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字を超える場合、リクエストが失敗する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A123456'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字の場合、リクエストが成功する', async () => { + const request = new CreateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A12345'; + + const valdationObject = plainToClass(CreateWorktypesRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + }); + + describe('valdation UpdateWorktypesRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdが指定されていない場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが空文字の場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = ''; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字を超える場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = '123456789A1234567'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('worktypeIdが16文字の場合、リクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = '123456789A123456'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('worktypeIdに使用不可文字が含まれる場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'test.test'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字を超える場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A123456'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('descriptionが255文字の場合、リクエストが成功する', async () => { + const request = new UpdateWorktypesRequest(); + request.worktypeId = 'TEST'; + request.description = + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A1234567A5A1234567A6A1234567A7A1234567A8A1234567A9A' + + '1234567B0B1234567B1B1234567B2B1234567B3B1234567B4B1234567B5B1234567B6B1234567B7B1234567B8B1234567B9B' + + '1234567A0A1234567A1A1234567A2A1234567A3A1234567A4A12345'; + const valdationObject = plainToClass(UpdateWorktypesRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + }); + + describe('valdation UpdateWorktypeRequestParam', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new UpdateWorktypeRequestParam(); + request.id = 1; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('idが1より小さい場合、リクエストが失敗する', async () => { + const request = new UpdateWorktypeRequestParam(); + request.id = 0; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idが数値でない場合、リクエストが失敗する', async () => { + const request = { id: '0' }; + + const valdationObject = plainToClass(UpdateWorktypeRequestParam, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + + describe('valdation switchParentRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('子アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = []; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('子アカウントが重複指定されている場合、リクエストが失敗する', async () => { + const request = new SwitchParentRequest(); + request.to = 1; + request.children = [2, 2]; + + const valdationObject = plainToClass(SwitchParentRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + + describe('valdation deletePartnerAccount', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new DeletePartnerAccountRequest(); + request.targetAccountId = 1; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('削除対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = new DeletePartnerAccountRequest(); + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('削除対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = new DeletePartnerAccountRequest(); + request.targetAccountId = 0; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('削除対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + class DeletePartnerAccountRequestString { + targetAccountId: string; + } + const request = new DeletePartnerAccountRequestString(); + request.targetAccountId = 'a'; + + const valdationObject = plainToClass( + DeletePartnerAccountRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + describe('valdation getPartnerUsers', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = { + targetAccountId: 1, + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('取得対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = {}; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('取得対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + userId: 0, + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('取得対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + userId: 'a', + }; + + const valdationObject = plainToClass(GetPartnerUsersRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + describe('valdation updatePartnerInfo', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('更新対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: undefined, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('更新対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 0, + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('更新対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 'a', + primaryAdminUserId: 2, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + // primaryAdminUserIdのテスト + it('更新対象アカウントが指定されていない場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: undefined, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('更新対象アカウントが0の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 0, + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('更新対象アカウントが文字列(数値以外)の場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 'a', + companyName: 'test', + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + // companyNameのテスト + it('更新対象アカウントが文字列以外場合、リクエストが失敗する', async () => { + const request = { + targetAccountId: 1, + primaryAdminUserId: 2, + companyName: 1, + }; + + const valdationObject = plainToClass(UpdatePartnerInfoRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); }); diff --git a/dictation_server/src/features/accounts/accounts.controller.ts b/dictation_server/src/features/accounts/accounts.controller.ts index e8a8ecd..9985180 100644 --- a/dictation_server/src/features/accounts/accounts.controller.ts +++ b/dictation_server/src/features/accounts/accounts.controller.ts @@ -71,6 +71,20 @@ import { DeleteWorktypeResponse, GetCompanyNameRequest, GetCompanyNameResponse, + DeleteTypistGroupRequestParam, + DeleteTypistGroupResponse, + UpdateFileDeleteSettingRequest, + UpdateFileDeleteSettingResponse, + UpdateRestrictionStatusRequest, + UpdateRestrictionStatusResponse, + SwitchParentRequest, + SwitchParentResponse, + DeletePartnerAccountRequest, + DeletePartnerAccountResponse, + GetPartnerUsersResponse, + GetPartnerUsersRequest, + UpdatePartnerInfoRequest, + UpdatePartnerInfoResponse, } from './types/types'; import { USER_ROLES, ADMIN_ROLES, TIERS } from '../../constants'; import { AuthGuard } from '../../common/guards/auth/authguards'; @@ -755,6 +769,86 @@ export class AccountsController { return {}; } + @ApiResponse({ + status: HttpStatus.OK, + type: DeleteTypistGroupResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'ルーティングルールに設定されている / タスクの割り当て候補に設定されている / 削除済み', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteTypistGroup', + description: + 'ログインしているユーザーのアカウント配下でIDで指定されたタイピストグループを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('typist-groups/:typistGroupId/delete') + async deleteTypistGroup( + @Req() req: Request, + @Param() param: DeleteTypistGroupRequestParam, + ): Promise { + const { typistGroupId } = param; + + // アクセストークン取得 + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.accountService.deleteTypistGroup(context, userId, typistGroupId); + + return {}; + } + @Post('partner') @ApiResponse({ status: HttpStatus.OK, @@ -1879,6 +1973,86 @@ export class AccountsController { return {}; } + @Post('me/file-delete-setting') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateFileDeleteSettingResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正/アカウント・ユーザー不在', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateFileDeleteSetting' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + }), + ) + async updateFileDeleteSetting( + @Req() req: Request, + @Body() body: UpdateFileDeleteSettingRequest, + ): Promise { + const { autoFileDelete, retentionDays } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.accountService.updateFileDeleteSetting( + context, + userId, + autoFileDelete, + retentionDays, + ); + + return {}; + } + @Post('/delete') @ApiResponse({ status: HttpStatus.OK, @@ -2076,4 +2250,407 @@ export class AccountsController { ); return companyName; } + + @ApiResponse({ + status: HttpStatus.OK, + type: UpdateRestrictionStatusResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'updateRestrictionStatus' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], tiers: [TIERS.TIER1] }), + ) + @Post('restriction-status') + async updateRestrictionStatus( + @Req() req: Request, + @Body() body: UpdateRestrictionStatusRequest, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // service層を呼び出す + const { accountId, restricted } = body; + await this.accountService.updateRestrictionStatus( + context, + accountId, + restricted, + ); + + return {}; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: SwitchParentResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'switchParent' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2], + }), + ) + @Post('parent/switch') + async switchParent( + @Req() req: Request, + @Body() body: SwitchParentRequest, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + const { to, children } = body; + await this.accountService.switchParent(context, to, children); + + return {}; + } + + @Post('partner/delete') + @ApiResponse({ + status: HttpStatus.OK, + type: DeletePartnerAccountResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + '実施者との親子関係不正や下位アカウント存在など削除実施条件に合致しない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ operationId: 'deletePartnerAccount' }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async deletePartnerAccount( + @Req() req: Request, + @Body() body: DeletePartnerAccountRequest, + ): Promise { + const { targetAccountId } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.accountService.deletePartnerAccount( + context, + userId, + targetAccountId, + ); + + return {}; + } + + @Post('partner/users') + @ApiResponse({ + status: HttpStatus.OK, + type: GetPartnerUsersResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'パラメータ不正/API実行者と取得対象が親子関係ではない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'getPartnerUsers', + description: + 'パートナーアカウントのユーザー情報を取得します(開発規約に基づき、他のAPIと合わせてGETではなくPOSTを使用)', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async getPartnerUsers( + @Req() req: Request, + @Body() body: GetPartnerUsersRequest, + ): Promise { + const { targetAccountId } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + const users = await this.accountService.getPartnerUsers( + context, + userId, + targetAccountId, + ); + + return { users }; + } + + @Post('partner/update') + @ApiResponse({ + status: HttpStatus.OK, + type: UpdatePartnerInfoResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'パラメータ不正/API実行者と取得対象が親子関係ではない/アカウントが不在/プライマリ管理者が同一アカウント内にいない', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'updatePartnerInfo', + description: 'パートナーアカウントの情報を更新します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ + roles: [ADMIN_ROLES.ADMIN], + tiers: [TIERS.TIER1, TIERS.TIER2, TIERS.TIER3], + }), + ) + async updatePartnerInfo( + @Req() req: Request, + @Body() body: UpdatePartnerInfoRequest, + ): Promise { + const { targetAccountId, primaryAdminUserId, companyName } = body; + + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.accountService.updatePartnerInfo( + context, + userId, + targetAccountId, + primaryAdminUserId, + companyName, + ); + + return {}; + } } diff --git a/dictation_server/src/features/accounts/accounts.service.spec.ts b/dictation_server/src/features/accounts/accounts.service.spec.ts index 6debabc..d185c6e 100644 --- a/dictation_server/src/features/accounts/accounts.service.spec.ts +++ b/dictation_server/src/features/accounts/accounts.service.spec.ts @@ -13,15 +13,18 @@ import { } from './test/accounts.service.mock'; import { makeDefaultConfigValue } from '../users/test/users.service.mock'; import { + createAudioFile, createLicense, createLicenseOrder, createLicenseSetExpiryDateAndStatus, createOptionItems, createWorktype, + getLicenseOrders, getOptionItems, - getSortCriteria, + getSortCriteriaList, getTypistGroup, getTypistGroupMember, + getTypistGroupMembers, getWorktypes, } from './test/utility'; import { DataSource } from 'typeorm'; @@ -37,15 +40,23 @@ import { getUser, getLicenses, getUserArchive, + getAccountArchive, + getTasks, + getSortCriteria, + getJobNumber, } from '../../common/test/utility'; import { AccountsService } from './accounts.service'; import { Context, makeContext } from '../../common/log'; import { ADB2C_SIGN_IN_TYPE, + INITIAL_JOB_NUMBER, LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, + MANUAL_RECOVERY_REQUIRED, OPTION_ITEM_VALUE_TYPE, + STORAGE_SIZE_PER_LICENSE, + TASK_STATUS, TIERS, USER_ROLES, WORKTYPE_MAX_COUNT, @@ -76,9 +87,23 @@ import { AdB2cUser } from '../../gateways/adb2c/types/types'; import { Worktype } from '../../repositories/worktypes/entity/worktype.entity'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; -import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflowTypists, + getWorkflows, +} from '../workflows/test/utility'; import { UsersService } from '../users/users.service'; import { truncateAllTable } from '../../common/test/init'; +import { createTask, getCheckoutPermissions } from '../tasks/test/utility'; +import { createCheckoutPermissions } from '../tasks/test/utility'; +import { TestLogger } from '../../common/test/logger'; +import { Account } from '../../repositories/accounts/entity/account.entity'; +import { + createTemplateFile, + getTemplateFiles, +} from '../templates/test/utility'; +import { createUserGroup } from '../users/test/utility'; describe('createAccount', () => { let source: DataSource | null = null; @@ -94,6 +119,8 @@ describe('createAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -134,7 +161,7 @@ describe('createAccount', () => { }, }); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -185,6 +212,8 @@ describe('createAccount', () => { expect(account?.country).toBe(country); expect(account?.parent_account_id).toBe(dealerAccountId); expect(account?.tier).toBe(TIERS.TIER5); + expect(account?.auto_file_delete).toBe(false); + expect(account?.file_retention_days).toBe(30); expect(account?.primary_admin_user_id).toBe(user?.id); expect(account?.secondary_admin_user_id).toBe(null); expect(user?.accepted_eula_version).toBe(acceptedEulaVersion); @@ -195,6 +224,16 @@ describe('createAccount', () => { expect(user?.account_id).toBe(accountId); expect(user?.role).toBe(role); + // jobNumberの初期値が正しく設定されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); + + // sortCriteriaが正しく設定されているか確認 + const sortCriteria = await getSortCriteria(source, user?.id ?? 0); + expect(sortCriteria?.user_id).toBe(user?.id); + expect(sortCriteria?.direction).toBe('ASC'); + expect(sortCriteria?.parameter).toBe('JOB_NUMBER'); + // 想定通りのメールが送られているか確認 expect(_subject).toBe('User Registration Notification [U-102]'); expect( @@ -389,7 +428,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -457,7 +496,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -528,7 +567,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -601,7 +640,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(1); const users = await getUsers(source); expect(users.length).toBe(1); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(1); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -686,7 +725,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(0); const users = await getUsers(source); expect(users.length).toBe(0); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(0); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -774,7 +813,7 @@ describe('createAccount', () => { expect(accounts.length).toBe(1); const users = await getUsers(source); expect(users.length).toBe(1); - const sortCriteria = await getSortCriteria(source); + const sortCriteria = await getSortCriteriaList(source); expect(sortCriteria.length).toBe(1); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith( @@ -804,6 +843,8 @@ describe('createPartnerAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -848,7 +889,26 @@ describe('createPartnerAccount', () => { }, }); - overrideSendgridService(service, {}); + let _subject = ''; + let _url: string | undefined = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + const urlPattern = /https?:\/\/[^\s]+/g; + const urls = text.match(urlPattern); + const url = urls?.pop(); + + _subject = subject; + _url = url; + }, + }); overrideBlobstorageService(service, { createContainer: async () => { @@ -888,6 +948,19 @@ describe('createPartnerAccount', () => { expect(createdAccount?.tier).toBe(2); expect(createdAccount?.primary_admin_user_id).toBe(createdUser?.id); expect(createdAccount?.secondary_admin_user_id).toBe(null); + const sortCriteria = await getSortCriteria(source, createdUser?.id ?? 0); + expect(sortCriteria).not.toBeNull(); + expect(sortCriteria?.user_id).toBe(createdUser?.id); + expect(sortCriteria?.direction).toBe('ASC'); + expect(sortCriteria?.parameter).toBe('JOB_NUMBER'); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); + + // 想定通りのメールが送られているか確認 + expect(_subject).toBe('User Registration Notification [U-114]'); + expect( + _url?.startsWith('http://localhost:8081/mail-confirm/user?verify='), + ).toBeTruthy(); } }); @@ -1278,6 +1351,10 @@ describe('createPartnerAccount', () => { expect(users.length).toBe(2); expect(users[0].external_id).toBe(parentExternalId); expect(users[1].external_id).toBe(partnerExternalId); + const sortCriteria = await getSortCriteriaList(source); + expect(sortCriteria.length).toBe(2); + const jobNumber = await getJobNumber(source, accounts[1].id); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); } @@ -1457,6 +1534,10 @@ describe('createPartnerAccount', () => { expect(users.length).toBe(2); expect(users[0].external_id).toBe(parentExternalId); expect(users[1].external_id).toBe(partnerExternalId); + const sortCriteria = await getSortCriteriaList(source); + expect(sortCriteria.length).toBe(2); + const jobNumber = await getJobNumber(source, accounts[1].id); + expect(jobNumber?.job_number).toBe(INITIAL_JOB_NUMBER); // ADB2Cユーザー削除メソッドが呼ばれているか確認 expect(b2cService.deleteUser).toBeCalledWith(partnerExternalId, context); // コンテナ削除メソッドが呼ばれているか確認 @@ -1802,6 +1883,8 @@ describe('getLicenseSummary', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1901,7 +1984,7 @@ describe('getLicenseSummary', () => { await createLicenseSetExpiryDateAndStatus( source, childAccountId1, - null, + new Date(2037, 1, 1, 23, 59, 59), 'Allocated', 1, ); @@ -1926,7 +2009,7 @@ describe('getLicenseSummary', () => { expiringWithin14daysLicense: 5, issueRequesting: 100, numberOfRequesting: 1, - storageSize: 0, + storageSize: 40000000000, usedSize: 0, shortage: 2, isStorageAvailable: false, @@ -1939,12 +2022,135 @@ describe('getLicenseSummary', () => { expiringWithin14daysLicense: 5, issueRequesting: 0, numberOfRequesting: 0, + storageSize: 25000000000, + usedSize: 0, + shortage: 0, + isStorageAvailable: false, + }); + }); + + it('第五階層のストレージ使用量が取得できる(ライセンスなし、音声ファイルなし)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + const service = module.get(AccountsService); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + const result = await service.getLicenseSummary(context, accountId); + expect(result).toEqual({ + totalLicense: 0, + allocatedLicense: 0, + reusableLicense: 0, + freeLicense: 0, + expiringWithin14daysLicense: 0, + issueRequesting: 0, + numberOfRequesting: 0, storageSize: 0, usedSize: 0, shortage: 0, isStorageAvailable: false, }); }); + + it('第五階層のストレージ使用量が取得できる(ライセンスあり、音声ファイルあり)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // audioFileを作成する + const fileSize1 = 15000; + await createAudioFile(source, accountId, 1, fileSize1); + const fileSize2 = 17000; + await createAudioFile(source, accountId, 1, fileSize2); + + // ライセンスを作成する + const reusableLicense = 3; + for (let i = 0; i < reusableLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.REUSABLE, + ); + } + + const allocatedLicense = 2; + for (let i = 0; i < allocatedLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.ALLOCATED, + i + 1, // なんでもよい。重複しないようにインクリメントする。 + ); + } + + const unallocatedLicense = 5; + for (let i = 0; i < unallocatedLicense; i++) { + await createLicenseSetExpiryDateAndStatus( + source, + accountId, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + } + + // 自アカウントだけに絞って計算出来ていることを確認するため、別のアカウントとaudioFileとライセンス作成する。 + const { id: otherAccountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company2', + }) + ).account; + + await createAudioFile(source, otherAccountId, 1, 5000); + await createLicenseSetExpiryDateAndStatus( + source, + otherAccountId, + new Date(2037, 1, 1, 23, 59, 59), + LICENSE_ALLOCATED_STATUS.ALLOCATED, + ); + + // テスト実行 + const service = module.get(AccountsService); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + const result = await service.getLicenseSummary(context, accountId); + + const expectedStorageSize = + (reusableLicense + allocatedLicense) * + STORAGE_SIZE_PER_LICENSE * + 1000 * + 1000 * + 1000; // 5GB + expect(result).toEqual({ + totalLicense: reusableLicense + unallocatedLicense, + allocatedLicense: allocatedLicense, + reusableLicense: reusableLicense, + freeLicense: unallocatedLicense, + expiringWithin14daysLicense: 0, + issueRequesting: 0, + numberOfRequesting: 0, + storageSize: expectedStorageSize, + usedSize: fileSize1 + fileSize2, + shortage: 0, + isStorageAvailable: false, + }); + }); }); describe('getPartnerAccount', () => { @@ -1961,6 +2167,8 @@ describe('getPartnerAccount', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2115,6 +2323,8 @@ describe('getOrderHistories', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2259,6 +2469,8 @@ describe('issueLicense', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2281,6 +2493,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); @@ -2378,6 +2593,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); const now = new Date(); // 親と子アカウントを作成する @@ -2477,6 +2695,9 @@ describe('issueLicense', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const now = new Date(); // 親と子アカウントを作成する const { id: parentAccountId } = ( @@ -2578,6 +2799,8 @@ describe('getDealers', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2645,7 +2868,7 @@ describe('getDealers', () => { ], }); }); - + it('非表示指定されたDealer以外のDealerを取得できる', async () => { if (!source) fail(); const module = await makeTestingModule(source); @@ -2702,6 +2925,8 @@ describe('createTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3014,6 +3239,8 @@ describe('getTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3226,6 +3453,8 @@ describe('updateTypistGroup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3640,6 +3869,321 @@ describe('updateTypistGroup', () => { }); }); +describe('deleteTypistGroup', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + }); + it('TypistGroupを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + } + + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + //実行結果を確認 + { + const typistGroups = await getTypistGroup(source, account.id); + expect(typistGroups.length).toBe(0); + + const typistGroupUsers = await getTypistGroupMembers(source); + expect(typistGroupUsers.length).toBe(0); + } + }); + it('タイピストグループが存在しない場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + // 作成したアカウントにユーザーを追加する + const user = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const typistGroupName = 'typist-group-name'; + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [user.id], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, 999); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015001')); + } else { + fail(); + } + } + }); + it('タイピストグループがルーティングルールに紐づいていた場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const workflow = await createWorkflow(source, account.id, authorUserId); + await createWorkflowTypist(source, workflow.id, undefined, group[0].id); + //作成したデータを確認 + { + const workflowTypists = await getWorkflowTypists(source, workflow.id); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(workflowTypists.length).toBe(1); + expect(workflowTypists[0].typist_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015002')); + } else { + fail(); + } + } + }); + it('タイピストグループがタスクのチェックアウト候補だった場合、400エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: authorId, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + const group = await getTypistGroup(source, account.id); + const { taskId } = await createTask( + source, + account.id, + authorUserId, + authorId, + 'worktypeId', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, taskId, undefined, group[0].id); + + //作成したデータを確認 + { + const checkoutPermission = await getCheckoutPermissions(source, taskId); + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toBe(group[0].id); + expect(groupUsers[0].user_id).toBe(typistUserId); + expect(checkoutPermission.length).toBe(1); + expect(checkoutPermission[0].user_group_id).toBe(group[0].id); + } + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E015003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + // 作成したアカウントにユーザーを追加する + const typiptUserExternalId = 'typist-user-external-id'; + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: typiptUserExternalId, + role: USER_ROLES.TYPIST, + }); + + const service = module.get(AccountsService); + const context = makeContext(admin.external_id, 'requestId'); + const typistGroupName = 'typist-group-name'; + await service.createTypistGroup( + context, + admin.external_id, + typistGroupName, + [typistUserId], + ); + + //作成したデータを確認 + const group = await getTypistGroup(source, account.id); + { + expect(group.length).toBe(1); + expect(group[0].name).toBe(typistGroupName); + const groupUsers = await getTypistGroupMember(source, group[0].id); + expect(groupUsers.length).toBe(1); + expect(groupUsers[0].user_group_id).toEqual(group[0].id); + expect(groupUsers[0].user_id).toEqual(typistUserId); + } + + //DBアクセスに失敗するようにする + const typistGroupService = module.get( + UserGroupsRepositoryService, + ); + typistGroupService.deleteTypistGroup = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTypistGroup(context, admin.external_id, group[0].id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + describe('getWorktypes', () => { let source: DataSource | null = null; beforeAll(async () => { @@ -3654,6 +4198,8 @@ describe('getWorktypes', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3786,6 +4332,8 @@ describe('createWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3951,6 +4499,8 @@ describe('updateWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4242,6 +4792,8 @@ describe('deleteWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4502,6 +5054,8 @@ describe('getOptionItems', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4653,6 +5207,8 @@ describe('updateOptionItems', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -4991,6 +5547,8 @@ describe('updateActiveWorktype', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5231,6 +5789,8 @@ describe('ライセンス発行キャンセル', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5285,6 +5845,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5349,6 +5912,9 @@ describe('ライセンス発行キャンセル', () => { ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await service.cancelIssue( makeContext('trackingId', 'requestId'), @@ -5387,6 +5953,9 @@ describe('ライセンス発行キャンセル', () => { }); const poNumber = 'CANCEL_TEST'; const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5434,6 +6003,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5481,6 +6053,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5529,6 +6104,9 @@ describe('ライセンス発行キャンセル', () => { null, ); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); overrideSendgridService(service, {}); await expect( service.cancelIssue( @@ -5557,6 +6135,8 @@ describe('パートナー一覧取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5737,6 +6317,8 @@ describe('アカウント情報更新', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -5758,7 +6340,7 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -5826,6 +6408,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5860,6 +6447,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5895,6 +6487,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5927,6 +6524,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -5958,6 +6560,11 @@ describe('アカウント情報更新', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, { sendMail: async () => { return; @@ -6000,6 +6607,8 @@ describe('getAccountInfo', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6029,6 +6638,9 @@ describe('getAccountInfo', () => { }); const service = module.get(AccountsService); + overrideAdB2cService(service, { + getUsers: async () => [], + }); const context = makeContext(admin.external_id, 'requestId'); const accountResponse = await service.getAccountInfo( @@ -6072,6 +6684,8 @@ describe('getAuthors', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6236,6 +6850,8 @@ describe('getTypists', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6430,6 +7046,8 @@ describe('deleteAccountAndData', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -6451,7 +7069,7 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); - let _subject: string = ''; + let _subject = ''; let _url: string | undefined = ''; overrideSendgridService(service, { sendMail: async ( @@ -6659,6 +7277,27 @@ describe('deleteAccountAndData', () => { ); expect(LicenseAllocationHistoryRecordA.length).toBe(0); + // 第五階層のアカウントAの管理者ユーザーが持つsortCriteriaが削除されていること + const sortCriteriaAccuntAAdmin = await getSortCriteria( + source, + tier5AccountsA.admin.id, + ); + expect(sortCriteriaAccuntAAdmin).toBe(null); + + // 第五階層のアカウントAの一般ユーザーが持つsortCriteriaが削除されていること + const sortCriteriaAccuntAUser = await getSortCriteria( + source, + userA?.id ?? 0, + ); + expect(sortCriteriaAccuntAUser).toBe(null); + + // 第五階層のアカウントAのJobNumberが削除されていること + const jobNumberAccuntA = await getJobNumber( + source, + tier5AccountsA.account.id, + ); + expect(jobNumberAccuntA).toBe(null); + // 第五階層のアカウントBは削除されていないこと const accountRecordB = await getAccount(source, tier5AccountsB.account.id); expect(accountRecordB?.id).not.toBeNull(); @@ -6683,6 +7322,11 @@ describe('deleteAccountAndData', () => { ); expect(LicenseAllocationHistoryRecordB.length).not.toBe(0); + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5AccountsA.account.id); + const UserArchive = await getUserArchive(source); expect(UserArchive.length).toBe(2); @@ -6693,6 +7337,31 @@ describe('deleteAccountAndData', () => { await getLicenseAllocationHistoryArchive(source); expect(LicenseAllocationHistoryArchive.length).toBe(1); + // 第五階層のアカウントBの管理者ユーザーが持つsortCriteriaが削除されていないこと + const sortCriteriaAccuntBAdmin = await getSortCriteria( + source, + tier5AccountsB.admin.id, + ); + expect(sortCriteriaAccuntBAdmin?.user_id).toBe(tier5AccountsB.admin.id); + expect(sortCriteriaAccuntBAdmin?.direction).toBe('ASC'); + expect(sortCriteriaAccuntBAdmin?.parameter).toBe('JOB_NUMBER'); + // 第五階層のアカウントBの一般ユーザーが持つsortCriteriaが削除されていないこと + const sortCriteriaAccuntBUser = await getSortCriteria( + source, + userB?.id ?? 0, + ); + expect(sortCriteriaAccuntBUser?.user_id).toBe(userB?.id ?? 0); + expect(sortCriteriaAccuntBUser?.direction).toBe('ASC'); + expect(sortCriteriaAccuntBUser?.parameter).toBe('FILE_LENGTH'); + + // 第五階層のアカウントBのJobNumberが削除されていないこと + const jobNumberAccuntB = await getJobNumber( + source, + tier5AccountsB.account.id, + ); + expect(jobNumberAccuntB?.account_id).toBe(tier5AccountsB.account.id); + expect(jobNumberAccuntB?.job_number).toBe('00000000'); + expect(_subject).toBe('Account Deleted Notification [U-111]'); expect(_url).toBe('http://localhost:8081/'); }); @@ -6701,6 +7370,14 @@ describe('deleteAccountAndData', () => { const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6727,11 +7404,6 @@ describe('deleteAccountAndData', () => { deleteAccountAndInsertArchives: jest.fn().mockRejectedValue(new Error()), }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6760,12 +7432,26 @@ describe('deleteAccountAndData', () => { expect(accountRecord?.id).not.toBeNull(); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord?.id).not.toBeNull(); + + // アーカイブが作成されていないことを確認 + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(0); + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(0); }); it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除失敗 + overrideAdB2cService(service, { + deleteUsers: jest.fn().mockRejectedValue(new Error()), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); // 第五階層のアカウント作成 @@ -6787,11 +7473,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除失敗 - overrideAdB2cService(service, { - deleteUsers: jest.fn().mockRejectedValue(new Error()), - }); - // blobstorageコンテナの削除成功 overrideBlobstorageService(service, { deleteContainer: jest.fn(), @@ -6815,12 +7496,31 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); it('blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了', async () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); const service = module.get(AccountsService); + // ADB2Cユーザーの削除成功 + overrideAdB2cService(service, { + deleteUsers: jest.fn(), + getUsers: jest.fn(), + getUser: async () => { + return { id: 'admin.external_id', displayName: 'admin' }; + }, + }); overrideSendgridService(service, {}); const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation(); @@ -6843,11 +7543,6 @@ describe('deleteAccountAndData', () => { account_id: tier5Accounts.account.id, }); - // ADB2Cユーザーの削除成功 - overrideAdB2cService(service, { - deleteUsers: jest.fn(), - }); - // blobstorageコンテナの削除失敗 overrideBlobstorageService(service, { deleteContainer: jest.fn().mockRejectedValue(new Error()), @@ -6871,6 +7566,17 @@ describe('deleteAccountAndData', () => { expect(accountRecord).toBe(null); const userRecord = await getUser(source, user?.id ?? 0); expect(userRecord).toBe(null); + + const accountArchive = await getAccountArchive(source); + expect(accountArchive.length).toBe(1); + const archive = accountArchive.at(0); + expect(archive?.id).toBe(tier5Accounts.account.id); + + const userArchive = await getUserArchive(source); + expect(userArchive.length).toBe(2); + const expectUserIds = [tier5Accounts.admin.id, user.id].sort(); + const userArchiveIds = userArchive.map((x) => x.id).sort(); + expect(expectUserIds).toStrictEqual(userArchiveIds); }); }); @@ -6888,6 +7594,8 @@ describe('getAccountInfoMinimalAccess', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7019,6 +7727,7 @@ describe('getAccountInfoMinimalAccess', () => { } }); }); + describe('getCompanyName', () => { let source: DataSource | null = null; beforeAll(async () => { @@ -7033,6 +7742,8 @@ describe('getCompanyName', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -7088,3 +7799,2639 @@ describe('getCompanyName', () => { } }); }); + +describe('updateFileDeleteSetting', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('自動削除の設定を更新できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account?.tier).toBe(5); + } + + // 更新するデータを設定 + const autoFileDelete = true; + const retentionDays = 100; + + await service.updateFileDeleteSetting( + context, + admin.external_id, + autoFileDelete, + retentionDays, + ); + + // 更新後データの確認 + { + const updatedAccount = await getAccount(source, account.id); + expect(updatedAccount?.auto_file_delete).toBe(autoFileDelete); + expect(updatedAccount?.file_retention_days).toBe(retentionDays); + } + }); + + it('対象アカウント非存在時に500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 更新するデータを設定 + const nonExistentId = `nonExistentId`; // 存在しないIDを指定 + const autoFileDelete = true; + const retentionDays = 100; + + const context = makeContext(nonExistentId, 'requestId'); + try { + await service.updateFileDeleteSetting( + context, + nonExistentId, + autoFileDelete, + retentionDays, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 作成したデータを確認 + { + const tier5Account = await getAccount(source, account.id); + expect(tier5Account?.tier).toBe(5); + } + + //DBアクセスに失敗するようにする + const usersRepositoryService = module.get( + UsersRepositoryService, + ); + usersRepositoryService.findUserByExternalId = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.updateFileDeleteSetting( + context, + admin.external_id, + true, + 100, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + +describe('updateRestrictionStatus', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('アカウント利用制限のON/OFFが出来る', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第一階層のアカウントを作成する + const { admin } = await makeTestAccount(source, { + tier: 1, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 操作対象の第五階層のアカウントを作成する + const { account } = await makeTestAccount(source, { + tier: 5, + locked: false, + }); + + const service = module.get(AccountsService); + // 利用制限をかけられるか確認。 + { + const restricted = true; + await service.updateRestrictionStatus(context, account.id, restricted); + + const result = await getAccount(source, account.id); + expect(result?.locked).toBe(restricted); + } + + // 利用制限を解除できるか確認。 + { + const restricted = false; + await service.updateRestrictionStatus(context, account.id, restricted); + + const result = await getAccount(source, account.id); + expect(result?.locked).toBe(restricted); + } + }); + + it('対象アカウントが存在しない場合は500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // アカウントを作成せずに実行する + const context = makeContext('dummy', 'requestId'); + try { + await service.updateRestrictionStatus(context, 0, false); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーを返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第一階層のアカウントを作成する + const { admin } = await makeTestAccount(source, { + tier: 1, + }); + const context = makeContext(admin.external_id, 'requestId'); + + // 操作対象の第五階層のアカウントを作成する + const { account } = await makeTestAccount(source, { + tier: 5, + locked: false, + }); + + //DBアクセスに失敗するようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + accountsRepositoryService.updateRestrictionStatus = jest + .fn() + .mockRejectedValue('DB failed'); + + // テスト実行する + const service = module.get(AccountsService); + try { + await service.updateRestrictionStatus(context, account.id, true); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + +describe('switchParent', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('第三階層<->第四階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 3, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 4, + country: `NZ`, // 同じリージョンで切り替えできることの確認 + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('第四階層<->第五階層間の階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 5, + country: `AU`, + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('第四<->第五の切り替えで、親子で国が異なる場合でも階層構造変更処理ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: newParent } = await makeTestAccount(source, { + tier: 4, + country: `AU`, + }); + + // 子アカウントを作成する + const { account: child1 } = await makeTestAccount(source, { + tier: 5, + country: `NZ`, + parent_account_id: undefined, + delegation_permission: true, + }); + + const { account: child2 } = await makeTestAccount(source, { + tier: 5, + country: `GB`, + parent_account_id: undefined, + delegation_permission: true, + }); + + // ライセンス注文作成 + await createLicenseOrder(source, child1.id, 10, 1); // 注文先アカウントは何でもいいため適当 + await createLicenseOrder(source, child1.id, 10, 1); // 親アカウントは何でもいいため適当 + await createLicenseOrder(source, child2.id, 10, 1); // 親アカウントは何でもいいため適当 + + // テスト実行 + const context = makeContext(`external_id`, 'requestId'); + const service = module.get(AccountsService); + await service.switchParent(context, newParent.id, [child1.id, child2.id]); + + const child1Result = await getAccount(source, child1.id); + const child2Result = await getAccount(source, child2.id); + const child1LicenseOrderResult = await getLicenseOrders(source, child1.id); + const child2LicenseOrderResult = await getLicenseOrders(source, child2.id); + + // アカウントテーブルの更新確認 + expect(child1Result?.parent_account_id).toBe(newParent.id); + expect(child1Result?.delegation_permission).toBe(false); + expect(child2Result?.parent_account_id).toBe(newParent.id); + expect(child2Result?.delegation_permission).toBe(false); + + // ライセンス注文が全てcancelされていることの確認 + expect(child1LicenseOrderResult.length).toBe(2); + const child1LicenseOrderStatuses = child1LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child1LicenseOrderStatuses).toBeTruthy(); + expect(child2LicenseOrderResult.length).toBe(1); + const child2LicenseOrderStatuses = child2LicenseOrderResult.every( + (x) => x.status === LICENSE_ISSUE_STATUS.CANCELED, + ); + expect(child2LicenseOrderStatuses).toBeTruthy(); + }); + + it('切り替え先親アカウントが存在しない場合は400エラー(親アカウント不在エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + try { + // 切り替え先アカウントを作成せずに実行する + await service.switchParent(context, 10, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017001')); + } else { + fail(); + } + } + }); + + it('切り替え先親アカウントが第三・第四以外の階層の場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child = new Account(); + child.id = 1; + child.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + // 親アカウントの階層が第五階層の場合に失敗する + const parent = new Account(); + parent.id = 10; + try { + parent.tier = 5; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第二階層の場合に失敗する + parent.tier = 2; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + + try { + // 親アカウントの階層が第一階層の場合に失敗する + parent.tier = 1; + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第五階層の子アカウントに対して、第三階層の切り替え先親アカウントを指定した場合は400エラー(階層関係不適切エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // 子アカウントの取得では1件だけ返すようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 5; + const child2 = new Account(); + child1.id = 1; + child1.tier = 4; + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017002')); + } else { + fail(); + } + } + }); + + it('第三<->第四の切り替えで、親子でリージョンが異なる場合は400エラー(リージョン関係不一致エラー)を返す', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + const child1 = new Account(); + child1.id = 1; + child1.tier = 4; + child1.country = 'AU'; + + const child2 = new Account(); + child2.id = 2; + child2.tier = 4; + child2.country = 'US'; // このアカウントだけリージョンが異なるようにしておく + + accountsRepositoryService.findAccountsById = jest + .fn() + .mockResolvedValue([child1, child2]); + + const context = makeContext('external_id', 'requestId'); + const service = module.get(AccountsService); + + const parent = new Account(); + parent.id = 10; + parent.tier = 3; + parent.country = 'AU'; + try { + accountsRepositoryService.findAccountById = jest + .fn() + .mockResolvedValue(parent); + await service.switchParent(context, parent.id, [child1.id, child2.id]); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E017003')); + } else { + fail(); + } + } + }); +}); + +describe('deletePartnerAccount', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウント情報が削除されること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); + } + }); + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('パートナーアカウントが親が子アカウントを持つ場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + // 第5階層のアカウント作成 + await makeTestAccount(source, { + parent_account_id: tier4Account.id, + tier: 5, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(4); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + try { + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E018001')); + } else { + fail(); + } + } + }); + it('ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: () => { + throw new Error('deleteUsers failed'); + }, + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('Blobコンテナの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: () => { + throw new Error('deleteContainer failed'); + }, + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + console.log(logs); + + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + + // パートナーアカウント削除完了通知が送信されていること + expect(_subject).toBe('Partner Account Deleted Notification [U-123]'); + + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe( + true, + ); + } + }); + it('メール送信失敗時でも処理続行すること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('sendMail failed'); + }, + }); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + // ライセンス + await createLicenseOrder( + source, + tier4Account.id, + tier3Account.id, + 100, + 'PO001', + ); + + await createLicenseSetExpiryDateAndStatus( + source, + tier4Account.id, + null, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + + // ワークタイプ + await createWorktype(source, tier4Account.id, 'worktype1'); + // タスク + await createTask( + source, + tier4Account.id, + tier4Admin.id, + tier4Admin.author_id ?? '', + '', + '00', + '00000001', + TASK_STATUS.UPLOADED, + ); + // ユーザーグループ + await createUserGroup(source, tier4Account.id, 'usergroup1', [typist.id]); + // テンプレートファイル + await createTemplateFile(source, tier4Account.id, 'template1', 'url'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // blobstorageコンテナの削除成功 + overrideBlobstorageService(service, { + deleteContainer: jest.fn(), + }); + + // 作成したデータを確認 + { + const tier4AccountRecord = await getAccount(source, tier4Account.id); + expect(tier4AccountRecord?.id).toBe(tier4Account.id); + expect(tier4AccountRecord?.tier).toBe(4); + const userRecord = await getUsers(source); + expect(userRecord.length).toBe(3); + const licenseRecord = await getLicenses(source, tier4Account.id); + expect(licenseRecord.length).toBe(1); + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(1); + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(1); + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(1); + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(1); + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(1); + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber?.job_number).toBe('00000000'); + expect(tier4AccountJobNumber?.account_id).toBe(tier4Account.id); + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountAdminSortCriteria?.parameter).toBe('JOB_NUMBER'); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria?.direction).toBe('ASC'); + expect(tier4AccountTypistSortCriteria?.parameter).toBe('FILE_LENGTH'); + } + + // パートナーアカウント情報の削除 + await service.deletePartnerAccount( + context, + tier3Admin.external_id, + tier4Account.id, + ); + // DB内が想定通りになっているか確認 + { + // パートナーアカウントが削除されていること + const account4Record = await getAccount(source, tier4Account.id); + expect(account4Record).toBe(null); + const userRecordA = await getUser(source, tier4Admin?.id ?? 0); + expect(userRecordA).toBe(null); + // パートナーアカウントのユーザーが削除されていること + const userRecord = await getUsers(source); + expect( + userRecord.filter((x) => x.account_id === tier4Account.id).length, + ).toBe(0); + // パートナーアカウントのJobNumberが削除されていること + const tier4AccountJobNumber = await getJobNumber(source, tier4Account.id); + expect(tier4AccountJobNumber).toBe(null); + // パートナーアカウントのソート条件が削除されていること + const tier4AccountAdminSortCriteria = await getSortCriteria( + source, + tier4Admin?.id ?? 0, + ); + expect(tier4AccountAdminSortCriteria).toBe(null); + const tier4AccountTypistSortCriteria = await getSortCriteria( + source, + typist.id, + ); + expect(tier4AccountTypistSortCriteria).toBe(null); + + // パートナーアカウントのライセンスが削除されていること + const licenseRecord = await source.manager.find(License, { + where: { account_id: tier4Account.id }, + }); + expect(licenseRecord.length).toBe(0); + // パートナーアカウントのライセンス注文履歴が削除されていること + const licenseOrderRecord = await source.manager.find(LicenseOrder, { + where: { from_account_id: tier4Account.id }, + }); + expect(licenseOrderRecord.length).toBe(0); + // パートナーアカウントのライセンス割り当て履歴が削除されていること + const LicenseAllocationHistoryRecord = await source.manager.find( + LicenseAllocationHistory, + { where: { account_id: tier4Account.id } }, + ); + expect(LicenseAllocationHistoryRecord.length).toBe(0); + + // パートナーアカウントのワークタイプが削除されていること + const worktypeRecord = await getWorktypes(source, tier4Account.id); + expect(worktypeRecord.length).toBe(0); + // パートナーアカウントのタスクが削除されていること + const taskRecord = await getTasks(source, tier4Account.id); + expect(taskRecord.length).toBe(0); + // パートナーアカウントのユーザーグループが削除されていること + const userGroupRecord = await getTypistGroup(source, tier4Account.id); + expect(userGroupRecord.length).toBe(0); + // パートナーアカウントのテンプレートファイルが削除されていること + const templateFileRecord = await getTemplateFiles( + source, + tier4Account.id, + ); + expect(templateFileRecord.length).toBe(0); + } + }); +}); +describe('getPartnerUsers', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウント情報が取得できること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { account: tier3Account, admin: tier3Admin } = await makeTestAccount( + source, + { tier: 3 }, + ); + // 第4階層のアカウント作成 + const { account: tier4Account, admin: tier4Admin } = await makeTestAccount( + source, + { + parent_account_id: tier3Account.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + const typist = await makeTestUser(source, { + account_id: tier4Account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + // パートナーアカウント情報の取得 + const partnerUsers = await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + expect(partnerUsers).toEqual([ + { + id: tier4Admin.id, + name: 'adb2c' + tier4Admin.external_id, + email: 'mail@example.com', + isPrimaryAdmin: true, + }, + { + id: typist.id, + name: 'adb2c' + typist.external_id, + email: 'mail@example.com', + isPrimaryAdmin: false, + }, + ]); + }); + + it('パートナーアカウントの親が実行者でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(AccountsService); + + // 第3階層のアカウント作成 + const { admin: tier3Admin } = await makeTestAccount(source, { tier: 3 }); + const { account: tier3Parent } = await makeTestAccount(source, { tier: 3 }); + // 第4階層のアカウント作成 + const { account: tier4Account } = await makeTestAccount( + source, + { + parent_account_id: tier3Parent.id, + tier: 4, + }, + { + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }, + ); + + const context = makeContext(tier3Admin.external_id, 'requestId'); + + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + deleteUsers: jest.fn(), + }); + + try { + // パートナーアカウント情報の取得 + await service.getPartnerUsers( + context, + tier3Admin.external_id, + tier4Account.id, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E019001')); + } else { + fail(); + } + } + }); +}); + +describe('updatePartnerInfo', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('パートナーアカウントの会社名を変更できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { + tier: 4, + parent_account_id: parent.id, + company_name: 'oldCompanyName', + }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('oldCompanyName'); + } + + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + 'newCompanyName', + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('newCompanyName'); + + // パートナーアカウント情報変更完了通知が送信されていること + expect(_subject).toBe('Partner Account Edit Notification [U-124]'); + } + }); + + it('パートナーアカウントのプライマリ管理者を変更できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { + tier: 4, + parent_account_id: parent.id, + }, + ); + const newPartnerAdmin = await makeTestUser(source, { + account_id: partner.id, + }); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + let _subject = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + newPartnerAdmin.id, + partner.company_name, + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(newPartnerAdmin.id); + + // パートナーアカウント情報変更完了通知が送信されていること + expect(_subject).toBe('Partner Account Edit Notification [U-124]'); + } + }); + + it('変更対象アカウントが実行者のパートナーアカウントでない場合、エラーなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4 }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + // empty + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + try { + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + partner.company_name, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E020001')); + } else { + fail(); + } + } + }); + + it('DBアクセスがエラーの場合、エラーなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4, parent_account_id: parent.id }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.primary_admin_user_id).toBe(partnerAdmin.id); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + // empty + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + //DBアクセスに失敗するようにする + const accountsRepositoryService = module.get( + AccountsRepositoryService, + ); + accountsRepositoryService.updatePartnerInfo = jest + .fn() + .mockRejectedValue('DB failed'); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + try { + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + partner.company_name, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + + it('メール送信に失敗した場合でも、エラーとならず成功となること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 新規親アカウントのアカウントを作成する + const { account: parent, admin: parentAdmin } = await makeTestAccount( + source, + { tier: 3 }, + { external_id: 'parent_external_id' }, + ); + + // 子アカウントを作成する + const { account: partner, admin: partnerAdmin } = await makeTestAccount( + source, + { tier: 4, parent_account_id: parent.id, company_name: 'oldCompanyName' }, + ); + + // 作成したデータを確認 + { + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('oldCompanyName'); + } + + const service = module.get(AccountsService); + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + throw new Error('sendMail failed'); + }, + }); + overrideAdB2cService(service, { + getUser: async (context, externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }, + getUsers: async (context, externalIds) => + externalIds.map((externalId) => { + return { + displayName: 'adb2c' + externalId, + id: externalId, + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'xxxxxx', + issuerAssignedId: 'mail@example.com', + }, + ], + }; + }), + }); + + // テスト実行 + const context = makeContext(parentAdmin.external_id, 'requestId'); + + await service.updatePartnerInfo( + context, + parentAdmin.external_id, + partner.id, + partnerAdmin.id, + 'newCompanyName', + ); + + { + // DB内が想定通りになっているか確認 + const partnerRecord = await getAccount(source, partner.id); + expect(partnerRecord?.company_name).toBe('newCompanyName'); + } + }); +}); diff --git a/dictation_server/src/features/accounts/accounts.service.ts b/dictation_server/src/features/accounts/accounts.service.ts index 94e7e3c..3277dc9 100644 --- a/dictation_server/src/features/accounts/accounts.service.ts +++ b/dictation_server/src/features/accounts/accounts.service.ts @@ -15,6 +15,9 @@ import { OPTION_ITEM_VALUE_TYPE, MANUAL_RECOVERY_REQUIRED, LICENSE_ISSUE_STATUS, + BLOB_STORAGE_REGION_AU, + BLOB_STORAGE_REGION_EU, + BLOB_STORAGE_REGION_US, } from '../../constants'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { @@ -34,6 +37,7 @@ import { Author, Partner, GetCompanyNameResponse, + PartnerUser, } from './types/types'; import { DateWithZeroTime, @@ -48,6 +52,9 @@ import { AccountNotFoundError, AdminUserNotFoundError, DealerAccountNotFoundError, + HierarchyMismatchError, + RegionMismatchError, + PartnerAccountDeletionError, } from '../../repositories/accounts/errors/types'; import { Context } from '../../common/log'; import { @@ -60,6 +67,8 @@ import { } from '../../repositories/licenses/errors/types'; import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -152,6 +161,12 @@ export class AccountsService { let shortage = allocatableLicenseWithMargin - expiringSoonLicense; shortage = shortage >= 0 ? 0 : Math.abs(shortage); + const { size, used } = await this.licensesRepository.getStorageInfo( + context, + accountId, + currentDate, + ); + const licenseSummaryResponse: GetLicenseSummaryResponse = { totalLicense, allocatedLicense, @@ -160,8 +175,8 @@ export class AccountsService { expiringWithin14daysLicense: expiringSoonLicense, issueRequesting, numberOfRequesting, - storageSize: 0, // XXX PBI1201対象外 - usedSize: 0, // XXX PBI1201対象外 + storageSize: size, + usedSize: used, shortage, isStorageAvailable, }; @@ -476,6 +491,8 @@ export class AccountsService { country: accountInfo.country, parentAccountId: accountInfo.parent_account_id ?? undefined, delegationPermission: accountInfo.delegation_permission, + autoFileDelete: accountInfo.auto_file_delete, + fileRetentionDays: accountInfo.file_retention_days, primaryAdminUserId: accountInfo.primary_admin_user_id ?? undefined, secondryAdminUserId: accountInfo.secondary_admin_user_id ?? undefined, parentAccountName: parentInfo ? parentInfo.company_name : undefined, @@ -1386,6 +1403,78 @@ export class AccountsService { } } + /** + * タイピストグループを削除する + * @param context + * @param externalId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + externalId: string, + typistGroupId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTypistGroup.name + } | params: { ` + + `externalId: ${externalId}, ` + + `typistGroupId: ${typistGroupId}, `, + ); + try { + // 外部IDをもとにユーザー情報を取得する + const { account_id } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + // タイピストグループを削除する + await this.userGroupsRepository.deleteTypistGroup( + context, + account_id, + typistGroupId, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // タイピストグループ削除済み + case TypistGroupNotExistError: + throw new HttpException( + makeErrorResponse('E015001'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがルーティングルールに使用されている + case AssignedWorkflowDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015002'), + HttpStatus.BAD_REQUEST, + ); + // タイピストグループがタスクの文字起こし候補に使用されている + case ExistsCheckoutPermissionDeleteFailedError: + throw new HttpException( + makeErrorResponse('E015003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTypistGroup.name}`, + ); + } + } + /** * ライセンス発行をキャンセルする * @param context @@ -2269,7 +2358,7 @@ export class AccountsService { country = targetAccount.country; } catch (e) { // アカウントの削除に失敗した場合はエラーを返す - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.deleteAccountAndData.name}`, ); @@ -2292,7 +2381,7 @@ export class AccountsService { ); } catch (e) { // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${accountId}, users_id: ${dbUsers.map( (x) => x.external_id, @@ -2308,7 +2397,7 @@ export class AccountsService { ); } catch (e) { // blobstorageコンテナを削除で失敗した場合は、MANUAL_RECOVERY_REQUIRED出して正常終了 - this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log(`[${context.getTrackingId()}] error=${e}`); this.logger.log( `${MANUAL_RECOVERY_REQUIRED}[${context.getTrackingId()}] Failed to delete blob container: ${accountId}, country: ${country}`, ); @@ -2498,4 +2587,656 @@ export class AccountsService { adminEmails: adminEmails, }; } + + /** + * 自動ファイル削除に関する設定を更新する + * @param context + * @param externalId + * @param isAutoFileDelete // ファイルの自動削除可否 + * @param retentionDays // ファイルの保持期間 + */ + async updateFileDeleteSetting( + context: Context, + externalId: string, + isAutoFileDelete: boolean, + retentionDays: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateFileDeleteSetting.name + } | params: { ` + + `externalId: ${externalId}, ` + + `autoFileDelete: ${isAutoFileDelete}, ` + + `retentionDays: ${retentionDays}, };`, + ); + + // アカウントテーブルの更新を行う + try { + // externalIdを基に自アカウントの情報を取得する + const { account_id: accountId } = + await this.usersRepository.findUserByExternalId(context, externalId); + + await this.accountRepository.updateFileDeleteSetting( + context, + accountId, + isAutoFileDelete, + retentionDays, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.updateFileDeleteSetting.name + }`, + ); + } + } + + /** + * 指定したアカウントのシステム利用制限状態を更新する + * @param context + * @param accountId 更新対象アカウントID + * @param restricted 制限するかどうか(true:制限する) + */ + async updateRestrictionStatus( + context: Context, + accountId: number, + restricted: boolean, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateRestrictionStatus.name + } | params: { ` + + `accountId: ${accountId}, ` + + `restricted: ${restricted},};`, + ); + + try { + await this.accountRepository.updateRestrictionStatus( + context, + accountId, + restricted, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // アカウントが存在しない場合のエラーもINTERNAL_SERVER_ERROR扱いのため個別の判定は行わない + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.updateRestrictionStatus.name + }`, + ); + } + } + + /** + * 指定した子アカウントの親アカウントを、指定した親アカウントに変更する + * @param context + * @param newParent + * @param children + * @returns parent + */ + async switchParent( + context: Context, + newParent: number, + children: number[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.switchParent.name + } | params: { ` + + `newParent: ${newParent}, ` + + `children: ${children.join(', ')}};`, + ); + + try { + // 切り替え対象の情報取得 + const childrenAccounts = await this.accountRepository.findAccountsById( + context, + children, + ); + if (childrenAccounts.length !== children.length) { + // 指定された子アカウントが一つでも存在しない場合は通常運用ではありえないので汎用エラー + throw new Error('Some children accounts are not found'); + } + + const parentAccount = await this.accountRepository.findAccountById( + context, + newParent, + ); + if (!parentAccount) { + // 指定された親アカウントが存在しない場合は通常運用で起こりうるため、BAD_REQUEST + throw new AccountNotFoundError( + `Parent account is not found. accountId=${newParent}`, + ); + } + + // 切り替え可否チェック(階層関係) + if ( + !this.isValidHierarchyRelation( + parentAccount.tier, + childrenAccounts.map((x) => x.tier), + ) + ) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. parentAccount.tier=${parentAccount.tier}`, + ); + } + // 切り替え可否チェック(リージョン・国関係) + const { success, errorType } = this.isValidLocationRelation( + parentAccount, + childrenAccounts, + ); + if (!success) { + throw errorType; + } + + // 切り替え処理実施 + await this.accountRepository.switchParentAccount( + context, + newParent, + children, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E017001'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E017002'), + HttpStatus.BAD_REQUEST, + ); + case RegionMismatchError: + throw new HttpException( + makeErrorResponse('E017003'), + HttpStatus.BAD_REQUEST, + ); + } + } + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.switchParent.name}`, + ); + } + } + + /** + * 切り替え対象の親アカウントと子アカウントの階層関係が正しいかどうかをチェックする + * @param parentTier + * @param childrenTiers + * @returns true if valid hierarchy relation + */ + private isValidHierarchyRelation( + parentTier: number, + childrenTiers: number[], + ): boolean { + // 全ての子アカウントの階層が、親アカウントの階層の一つ下であるかつ、第三<->第四または第四<->第五の切り替えの場合のみ判定OK。 + if ( + parentTier === TIERS.TIER3 && + childrenTiers.every((child) => child === TIERS.TIER4) + ) { + return true; + } else if ( + parentTier === TIERS.TIER4 && + childrenTiers.every((child) => child === TIERS.TIER5) + ) { + return true; + } + + return false; + } + + /** + * 切り替え対象の親アカウントと子アカウントのリージョン・国関係が正しいかどうかをチェックする。 + * @param parent + * @param children + * @returns valid location relation + */ + private isValidLocationRelation( + parent: Account, + children: Account[], + ): { + success: boolean; + errorType: null | RegionMismatchError; + } { + // 「プロダクト バックログ項目 4171: 階層構造切り替えのリージョンの制約を修正する」の対応で第五階層の親(第四階層)を切り替えるときはリージョン・国の制約をなくす。 2024年5月16日 + // 第三<->第四の切り替えはリージョンの一致を確認する。 + if (parent.tier === TIERS.TIER3) { + if ( + !children.every( + (child) => + this.getRegion(child.country) === this.getRegion(parent.country), + ) + ) { + return { + success: false, + errorType: new RegionMismatchError('Invalid region relation'), + }; + } + + return { success: true, errorType: null }; + } else if (parent.tier === TIERS.TIER4) { + // 第四<->第五の切り替えは国・リージョンの一致を確認しない。 + return { success: true, errorType: null }; + } else { + // 親アカウントの階層が想定外の場合、本関数の使い方が間違っているので例外を投げる + throw new Error('Not implemented'); + } + } + + /** + * 国の所属する地域を取得する。 + * @param country + * @returns region + */ + private getRegion(country: string): string { + // OMDS様より、地域はBlobStorageのリージョンで判定するでOKとのこと。 + if (BLOB_STORAGE_REGION_AU.includes(country)) { + return 'AU'; + } else if (BLOB_STORAGE_REGION_EU.includes(country)) { + return 'EU'; + } else if (BLOB_STORAGE_REGION_US.includes(country)) { + return 'US'; + } else { + // ここに到達する場合は、国が想定外の値であるため、例外を投げる + throw new Error(`Invalid country. country=${country}`); + } + } + /** + * 指定したアカウントIDのパートナーアカウントを削除する + * @param context + * @param externalId + * @param targetAccountId 削除対象パートナーのアカウントID + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deletePartnerAccount.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: parentAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (parentAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + + // 削除対象のパートナーアカウントを取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + if (targetAccount === null) { + throw new AccountNotFoundError( + `Account not found. targetAccountId: ${targetAccountId}`, + ); + } + + // メール送信に必要な情報を取得する + if (!targetAccount.primary_admin_user_id) { + throw new Error( + `primary_admin_user_id not found. accountId: ${targetAccountId}`, + ); + } + const primaryAdminUser = await this.usersRepository.findUserById( + context, + targetAccount.primary_admin_user_id, + ); + const adb2cAdmin = await this.adB2cService.getUser( + context, + primaryAdminUser.external_id, + ); + const { + displayName: targetPrimaryAdminName, + emailAddress: targetPrimaryAdminEmail, + } = getUserNameAndMailAddress(adb2cAdmin); + + if (!targetPrimaryAdminEmail) { + throw new Error( + `adb2c user mail not found. externalId: ${primaryAdminUser.external_id}`, + ); + } + + // アカウント削除処理(DB) + const targetUsers = await this.accountRepository.deletePartnerAccount( + context, + parentAccount.id, + targetAccountId, + ); + + // アカウント削除処理(Azure AD B2C) + try { + // 削除対象アカウント内のADB2Cユーザーをすべて削除する + await this.adB2cService.deleteUsers( + targetUsers.map((x) => x.external_id), + context, + ); + this.logger.log( + `[${context.getTrackingId()}] delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } catch (e) { + // ADB2Cユーザーの削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete ADB2C users: ${targetAccountId}, users_id: ${targetUsers.map( + (x) => x.external_id, + )}`, + ); + } + + // アカウント削除処理(Blob Storage) + await this.deleteBlobContainer( + targetAccountId, + targetAccount.country, + context, + ); + + // メール送信処理 + try { + const { companyName: parentCompanyName, adminEmails: parentEmails } = + await this.getAccountInformation(context, parentAccount.id); + + await this.sendgridService.sendMailWithU123( + context, + targetAccount.company_name, + targetPrimaryAdminName, + targetPrimaryAdminEmail, + parentCompanyName, + parentEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case PartnerAccountDeletionError: + throw new HttpException( + makeErrorResponse('E018001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deletePartnerAccount.name}`, + ); + } + } + /** + * 指定したアカウントIDのユーザー情報を取得します + * @param context + * @param targetAccountId 取得対象パートナーのアカウントID + * @returns PartnerUser[] + */ + async getPartnerUsers( + context: Context, + externalId: string, + targetAccountId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.getPartnerUsers.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId},};`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: myAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (myAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + // 指定したアカウントIDの情報を取得する + const targetAccount = await this.accountRepository.findAccountById( + context, + targetAccountId, + ); + + // 実行者のアカウントが対象アカウントの親アカウントであるか確認する。 + if (myAccount.id !== targetAccount.parent_account_id) { + throw new HierarchyMismatchError( + `Invalid hierarchy relation. accountId: ${targetAccountId}`, + ); + } + // 対象アカウントのユーザ一覧を取得する + const users = await this.usersRepository.findUsersByAccountId( + context, + targetAccountId, + ); + + //ADB2Cからユーザー情報を取得する + const externalIds = users.map((x) => x.external_id); + const adb2cUsers = await this.adB2cService.getUsers(context, externalIds); + + // ユーザー情報をマージする + const partnerUsers = users.map((user) => { + const adb2cUser = adb2cUsers.find( + (adb2c) => user.external_id === adb2c.id, + ); + if (!adb2cUser) { + throw new Error( + `adb2c user not found. externalId: ${user.external_id}`, + ); + } + const { displayName, emailAddress } = + getUserNameAndMailAddress(adb2cUser); + if (!emailAddress) { + throw new Error( + `adb2c user mail not found. externalId: ${user.external_id}`, + ); + } + return { + id: user.id, + name: displayName, + email: emailAddress, + isPrimaryAdmin: targetAccount.primary_admin_user_id === user.id, + }; + }); + + return partnerUsers; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E019001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.getPartnerUsers.name}`, + ); + } + } + /** + * 指定したパートナーアカウントの情報を更新する + * @param context + * @param externalId + * @param targetAccountId // 更新対象のアカウントID + * @param primaryAdminUserId // 更新後のプライマリ管理者のユーザーID + * @param companyName // 更新後の会社名 + * @returns partner account + */ + async updatePartnerInfo( + context: Context, + externalId: string, + targetAccountId: number, + primaryAdminUserId: number, + companyName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updatePartnerInfo.name + } | params: { ` + + `externalId: ${externalId}, ` + + `targetAccountId: ${targetAccountId}, ` + + `primaryAdminUserId: ${primaryAdminUserId}, ` + + `companyName: ${companyName}, };`, + ); + + try { + // 外部IDをもとにユーザー情報を取得する + const { account: parentAccount } = + await this.usersRepository.findUserByExternalId(context, externalId); + + if (parentAccount === null) { + throw new AccountNotFoundError( + `account not found. externalId: ${externalId}`, + ); + } + + // アカウント情報更新処理 + await this.accountRepository.updatePartnerInfo( + context, + parentAccount.id, + targetAccountId, + primaryAdminUserId, + companyName, + ); + + // メール送信処理 + try { + // 実行者のアカウント情報 + const { companyName: parentCompanyName, adminEmails: parentEmails } = + await this.getAccountInformation(context, parentAccount.id); + + // 更新後のパートナーのプライマリ管理者 + const primaryAdmin = await this.usersRepository.findUserById( + context, + primaryAdminUserId, + ); + const adb2cAdmin = await this.adB2cService.getUser( + context, + primaryAdmin.external_id, + ); + const { + displayName: primaryAdminName, + emailAddress: primaryAdminEmail, + } = getUserNameAndMailAddress(adb2cAdmin); + + if (!primaryAdminEmail) { + throw new Error( + `adb2c user mail not found. externalId: ${primaryAdmin.external_id}`, + ); + } + + await this.sendgridService.sendMailWithU124( + context, + companyName, + primaryAdminName, + primaryAdminEmail, + parentCompanyName, + parentEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case AccountNotFoundError: + throw new HttpException( + makeErrorResponse('E010501'), + HttpStatus.BAD_REQUEST, + ); + case AdminUserNotFoundError: + throw new HttpException( + makeErrorResponse('E010502'), + HttpStatus.BAD_REQUEST, + ); + case HierarchyMismatchError: + throw new HttpException( + makeErrorResponse('E020001'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.updatePartnerInfo.name}`, + ); + } + } } diff --git a/dictation_server/src/features/accounts/test/accounts.service.mock.ts b/dictation_server/src/features/accounts/test/accounts.service.mock.ts index a031dae..58b1dd1 100644 --- a/dictation_server/src/features/accounts/test/accounts.service.mock.ts +++ b/dictation_server/src/features/accounts/test/accounts.service.mock.ts @@ -42,6 +42,7 @@ export type UserGroupsRepositoryMockValue = { export type AdB2cMockValue = { createUser: string | ConflictError | Error; getUsers: AdB2cUser[] | Error; + getUser: AdB2cUser | Error; }; export type SendGridMockValue = { sendMail: undefined | Error; @@ -206,7 +207,7 @@ export const makeUserGroupsRepositoryMock = ( }; }; export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { - const { createUser, getUsers } = value; + const { createUser, getUsers, getUser } = value; return { createUser: @@ -219,6 +220,10 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { getUsers instanceof Error ? jest.fn, []>().mockRejectedValue(getUsers) : jest.fn, []>().mockResolvedValue(getUsers), + getUser: + getUser instanceof Error + ? jest.fn, []>().mockRejectedValue(getUser) + : jest.fn, []>().mockResolvedValue(getUser), }; }; export const makeConfigMock = (value: ConfigMockValue) => { @@ -437,6 +442,10 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { displayName: 'Typist3', }, ], + getUser: { + id: 'typist1', + displayName: 'Typist1', + }, }; }; export const makeDefaultSendGridlValue = (): SendGridMockValue => { diff --git a/dictation_server/src/features/accounts/test/utility.ts b/dictation_server/src/features/accounts/test/utility.ts index e658d9b..473a258 100644 --- a/dictation_server/src/features/accounts/test/utility.ts +++ b/dictation_server/src/features/accounts/test/utility.ts @@ -10,13 +10,14 @@ import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity import { OptionItem } from '../../../repositories/worktypes/entity/option_item.entity'; import { OPTION_ITEM_VALUE_TYPE } from '../../../constants'; import { Account } from '../../../repositories/accounts/entity/account.entity'; +import { AudioFile } from '../../../repositories/audio_files/entity/audio_file.entity'; /** * テスト ユーティリティ: すべてのソート条件を取得する * @param dataSource データソース * @returns 該当ソート条件一覧 */ -export const getSortCriteria = async (dataSource: DataSource) => { +export const getSortCriteriaList = async (dataSource: DataSource) => { return await dataSource.getRepository(SortCriteria).find(); }; @@ -123,6 +124,13 @@ export const getTypistGroupMember = async ( }); }; +// タイピストグループメンバー一覧を取得する +export const getTypistGroupMembers = async ( + datasource: DataSource, +): Promise => { + return await datasource.getRepository(UserGroupMember).find(); +}; + // Worktypeを作成する export const createWorktype = async ( datasource: DataSource, @@ -212,3 +220,43 @@ export const getOptionItems = async ( }) : await datasource.getRepository(OptionItem).find(); }; + +export const createAudioFile = async ( + datasource: DataSource, + account_id: number, + owner_user_id: number, + fileSize: number, +): Promise<{ audioFileId: number }> => { + const { identifiers: audioFileIdentifiers } = await datasource + .getRepository(AudioFile) + .insert({ + account_id: account_id, + owner_user_id: owner_user_id, + url: '', + file_name: 'x.zip', + author_id: 'author_id', + work_type_id: '', + started_at: new Date(), + duration: '100000', + finished_at: new Date(), + uploaded_at: new Date(), + file_size: fileSize, + priority: '00', + audio_format: 'audio_format', + is_encrypted: true, + }); + const audioFile = audioFileIdentifiers.pop() as AudioFile; + return { audioFileId: audioFile.id }; +}; + +// ライセンス注文を取得する +export const getLicenseOrders = async ( + datasource: DataSource, + accountId: number, +): Promise => { + return await datasource.getRepository(LicenseOrder).find({ + where: { + from_account_id: accountId, + }, + }); +}; diff --git a/dictation_server/src/features/accounts/types/types.ts b/dictation_server/src/features/accounts/types/types.ts index 2648785..2363279 100644 --- a/dictation_server/src/features/accounts/types/types.ts +++ b/dictation_server/src/features/accounts/types/types.ts @@ -12,6 +12,9 @@ import { IsIn, ArrayMaxSize, ValidateNested, + Max, + IsString, + IsNotEmpty, } from 'class-validator'; import { IsAdminPasswordvalid } from '../../../common/validators/admin.validator'; import { IsUnique } from '../../../common/validators/IsUnique.validator'; @@ -120,6 +123,14 @@ export class UpdateTypistGroupRequestParam { @Min(1) typistGroupId: number; } +export class DeleteTypistGroupRequestParam { + @ApiProperty() + @Type(() => Number) + @IsInt() + @Min(1) + typistGroupId: number; +} + export class CreatePartnerAccountRequest { @ApiProperty() @MaxLength(255) @@ -348,6 +359,72 @@ export class GetCompanyNameRequest { accountId: number; } +export class UpdateRestrictionStatusRequest { + @ApiProperty({ description: '操作対象の第五階層アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + accountId: number; + + @ApiProperty({ description: '制限をかけるかどうか(trur:制限をかける)' }) + @Type(() => Boolean) + restricted: boolean; +} + +export class SwitchParentRequest { + @ApiProperty({ description: '切り替え先の親アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + to: number; + + @ApiProperty({ + minItems: 1, + isArray: true, + type: 'integer', + description: '親を変更したいアカウントIDのリスト', + }) + @ArrayMinSize(1) + @IsArray() + @IsInt({ each: true }) + @Min(1, { each: true }) + @IsUnique() + children: number[]; +} + +export class DeletePartnerAccountRequest { + @ApiProperty({ description: '削除対象のアカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; +} + +export class GetPartnerUsersRequest { + @ApiProperty({ description: '取得対象のアカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; +} + +export class UpdatePartnerInfoRequest { + @ApiProperty({ description: '変更対象アカウントID' }) + @Type(() => Number) + @IsInt() + @Min(1) + targetAccountId: number; + + @ApiProperty({ description: 'プライマリ管理者ID' }) + @Type(() => Number) + @IsInt() + @Min(1) + primaryAdminUserId: number; + + @ApiProperty({ description: '会社名' }) + @MaxLength(255) + companyName: string; +} // ============================== // RESPONSE // ============================== @@ -413,6 +490,12 @@ export class Account { @ApiProperty() delegationPermission: boolean; + @ApiProperty() + autoFileDelete: boolean; + + @ApiProperty() + fileRetentionDays: number; + @ApiProperty({ required: false }) primaryAdminUserId?: number; @@ -481,6 +564,8 @@ export class CreateTypistGroupResponse {} export class UpdateTypistGroupResponse {} +export class DeleteTypistGroupResponse {} + export class CreatePartnerAccountResponse {} export class PartnerLicenseInfo { @@ -637,6 +722,60 @@ export class GetAccountInfoMinimalAccessResponse { tier: number; } +export class UpdateFileDeleteSettingRequest { + @ApiProperty({ description: '自動ファイル削除をするかどうか' }) + @Type(() => Boolean) + autoFileDelete: boolean; + + @ApiProperty({ + description: + '文字起こし完了してから自動ファイル削除されるまでのファイルの保存期間', + }) + @Type(() => Number) + @IsInt() + @Min(1) + @Max(999) + retentionDays: number; +} + +export class UpdateFileDeleteSettingResponse {} + +export class UpdateRestrictionStatusResponse {} + +export class SwitchParentResponse {} + +export class DeletePartnerAccountResponse {} +export class PartnerUser { + @ApiProperty({ description: 'ユーザーID' }) + @Type(() => Number) + @IsInt() + @IsNotEmpty() + id: number; + + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: 'プライマリ管理者かどうか' }) + @Type(() => Boolean) + isPrimaryAdmin: boolean; +} + +export class GetPartnerUsersResponse { + @ApiProperty({ type: [PartnerUser] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PartnerUser) + users: PartnerUser[]; +} + +export class UpdatePartnerInfoResponse {} // ============================== // Request/Response外の型 // TODO: Request/Response/その他の型を別ファイルに分ける diff --git a/dictation_server/src/features/auth/auth.controller.spec.ts b/dictation_server/src/features/auth/auth.controller.spec.ts index a762b74..aa4aeae 100644 --- a/dictation_server/src/features/auth/auth.controller.spec.ts +++ b/dictation_server/src/features/auth/auth.controller.spec.ts @@ -6,6 +6,9 @@ import { makeDefaultAdB2cMockValue, } from './test/auth.service.mock'; import { ConfigModule } from '@nestjs/config'; +import { TokenRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('AuthController', () => { let controller: AuthController; @@ -30,4 +33,57 @@ describe('AuthController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation token', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('idTokenが指定されていない場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idTokenが空文字の場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = ''; + request.type = 'web'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('idTokenが文字列でない場合、リクエストが失敗する', async () => { + const request = { idToken: 1, type: 'web' }; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('typeが指定されていない場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('typeがweb,mobile,desktop以外の場合、リクエストが失敗する', async () => { + const request = new TokenRequest(); + request.idToken = 'test'; + request.type = 'invalid'; + + const valdationObject = plainToClass(TokenRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/auth/auth.service.spec.ts b/dictation_server/src/features/auth/auth.service.spec.ts index 85473b3..7843214 100644 --- a/dictation_server/src/features/auth/auth.service.spec.ts +++ b/dictation_server/src/features/auth/auth.service.spec.ts @@ -21,6 +21,7 @@ import { TIERS, USER_ROLES } from '../../constants'; import { decode, isVerifyError } from '../../common/jwt'; import { RefreshToken, AccessToken } from '../../common/token'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('AuthService', () => { it('IDトークンの検証とペイロードの取得に成功する', async () => { @@ -175,6 +176,8 @@ describe('checkIsAcceptedLatestVersion', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -350,6 +353,8 @@ describe('generateDelegationRefreshToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -496,6 +501,8 @@ describe('generateDelegationAccessToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -607,6 +614,8 @@ describe('updateDelegationAccessToken', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/auth/test/utility.ts b/dictation_server/src/features/auth/test/utility.ts index fbbc0e4..fc78418 100644 --- a/dictation_server/src/features/auth/test/utility.ts +++ b/dictation_server/src/features/auth/test/utility.ts @@ -2,6 +2,7 @@ import { DataSource } from 'typeorm'; import { Term } from '../../../repositories/terms/entity/term.entity'; import { Account } from '../../../repositories/accounts/entity/account.entity'; import { User } from '../../../repositories/users/entity/user.entity'; +import { JobNumber } from '../../../repositories/job_number/entity/job_number.entity'; export const createTermInfo = async ( datasource: DataSource, @@ -34,5 +35,6 @@ export const deleteAccount = async ( id: number, ): Promise => { await dataSource.getRepository(User).delete({ account_id: id }); + await dataSource.getRepository(JobNumber).delete({ account_id: id }); await dataSource.getRepository(Account).delete({ id: id }); }; diff --git a/dictation_server/src/features/files/files.controller.spec.ts b/dictation_server/src/features/files/files.controller.spec.ts index 3986b38..e5b87e6 100644 --- a/dictation_server/src/features/files/files.controller.spec.ts +++ b/dictation_server/src/features/files/files.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FilesController } from './files.controller'; import { FilesService } from './files.service'; import { ConfigModule } from '@nestjs/config'; +import { AudioUploadFinishedRequest, FileRenameRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('FilesController', () => { let controller: FilesController; @@ -28,3 +31,269 @@ describe('FilesController', () => { expect(controller).toBeDefined(); }); }); + +describe('valdation FileRenameRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('音声ファイルIDが指定されていない場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが0の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 0; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイルIDが文字列の場合、リクエストが失敗する', async () => { + class InvalidFileRenameRequest { + audioFileId: string; + fileName: string; + } + + const request = new InvalidFileRenameRequest(); + request.audioFileId = 'invalid'; + request.fileName = 'fileName'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が空文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = ''; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('音声ファイル名が64文字の場合、リクエストに成功する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = + 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCD'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('音声ファイル名が65文字の場合、リクエストが失敗する', async () => { + const request = new FileRenameRequest(); + request.audioFileId = 1; + request.fileName = + 'ABCDEFGHI1ABCDEFGHI2ABCDEFGHI3ABCDEFGHI4ABCDEFGHI5ABCDEFGHI6ABCDE'; + + const valdationObject = plainToClass(FileRenameRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); + +describe('valdation AudioUploadFinishedRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('必須パラメータが空文字の場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = ''; //空文字 + request.authorId = ''; //空文字 + request.fileName = ''; //空文字 + request.duration = ''; //空文字 + request.createdDate = ''; //空文字 + request.finishedDate = ''; //空文字 + request.uploadedDate = ''; //空文字 + request.priority = ''; //空文字 + // fileSizeは設定しない + request.audioFormat = ''; //空文字 + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(10); + }); + it('durationが数値変換不可の場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = 'aaaaa'; //数値変換不可 + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('fileSizeが数値でない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 'aaaa' as any; //数値でない + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('fileSizeがintではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 5.01; //integerでない + request.priority = '00'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('priorityが00,01ではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '001'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('optionItemListが10件ではない場合、リクエストが失敗する', async () => { + const request = new AudioUploadFinishedRequest(); + request.url = 'url'; + request.authorId = 'authorId'; + request.fileName = 'fileName'; + request.duration = '10'; + request.createdDate = '2024-12-31T23:59:59.999Z'; + request.finishedDate = '2024-12-31T23:59:59.999Z'; + request.uploadedDate = '2024-12-31T23:59:59.999Z'; + request.fileSize = 1; + request.priority = '01'; + request.audioFormat = 'DSS'; + request.comment = 'comment'; + request.workType = 'workType'; + request.optionItemList = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => { + return { + optionItemLabel: `optionItemLabel${i}`, + optionItemValue: `optionItemValue${i}`, + }; + }); + request.isEncrypted = false; + + const valdationObject = plainToClass(AudioUploadFinishedRequest, request); + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); diff --git a/dictation_server/src/features/files/files.controller.ts b/dictation_server/src/features/files/files.controller.ts index f8b4328..ceb28ac 100644 --- a/dictation_server/src/features/files/files.controller.ts +++ b/dictation_server/src/features/files/files.controller.ts @@ -27,6 +27,8 @@ import { AudioUploadFinishedResponse, AudioUploadLocationRequest, AudioUploadLocationResponse, + FileRenameRequest, + FileRenameResponse, TemplateDownloadLocationRequest, TemplateDownloadLocationResponse, TemplateUploadFinishedReqponse, @@ -533,4 +535,74 @@ export class FilesController { await this.filesService.templateUploadFinished(context, userId, url, name); return {}; } + + @ApiResponse({ + status: HttpStatus.OK, + type: FileRenameResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'fileRename', + description: '音声ファイルの表示ファイル名を変更します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @Post('rename') + async fileRename( + @Req() req: Request, + @Body() body: FileRenameRequest, + ): Promise { + const { audioFileId, fileName } = body; + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + await this.filesService.fileRename(context, userId, audioFileId, fileName); + return {}; + } } diff --git a/dictation_server/src/features/files/files.module.ts b/dictation_server/src/features/files/files.module.ts index 56ba499..2a7e224 100644 --- a/dictation_server/src/features/files/files.module.ts +++ b/dictation_server/src/features/files/files.module.ts @@ -10,6 +10,9 @@ import { TemplateFilesRepositoryModule } from '../../repositories/template_files import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_groups.repository.module'; import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; +import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; +import { AdB2cModule } from '../../gateways/adb2c/adb2c.module'; +import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; @Module({ imports: [ @@ -22,6 +25,10 @@ import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.r UserGroupsRepositoryModule, NotificationhubModule, LicensesRepositoryModule, + SendGridModule, + AdB2cModule, + AccountsRepositoryModule, + AudioFilesRepositoryModule, ], providers: [FilesService], controllers: [FilesController], diff --git a/dictation_server/src/features/files/files.service.spec.ts b/dictation_server/src/features/files/files.service.spec.ts index e0a1ccd..813b155 100644 --- a/dictation_server/src/features/files/files.service.spec.ts +++ b/dictation_server/src/features/files/files.service.spec.ts @@ -13,13 +13,20 @@ import { import { FilesService } from './files.service'; import { makeContext } from '../../common/log'; import { + createJobNumber, + getJobNumber, + getTasks, makeHierarchicalAccounts, makeTestAccount, makeTestSimpleAccount, makeTestUser, + updateJobNumber, } from '../../common/test/utility'; import { makeTestingModule } from '../../common/test/modules'; -import { overrideBlobstorageService } from '../../common/test/overrides'; +import { + overrideAdB2cService, + overrideBlobstorageService, +} from '../../common/test/overrides'; import { createTemplateFile, getTemplateFiles, @@ -33,7 +40,11 @@ import { import { createWorktype } from '../accounts/test/utility'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service'; -import { getCheckoutPermissions, getTask } from '../tasks/test/utility'; +import { + createCheckoutPermissions, + getAudioFile, + getCheckoutPermissions, +} from '../tasks/test/utility'; import { DateWithZeroTime } from '../licenses/types/types'; import { LICENSE_ALLOCATED_STATUS, @@ -42,6 +53,8 @@ import { USER_ROLES, } from '../../constants'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; +import { TasksService } from '../tasks/tasks.service'; describe('publishUploadSas', () => { let source: DataSource | null = null; @@ -57,6 +70,8 @@ describe('publishUploadSas', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -252,6 +267,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -310,6 +327,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -323,6 +344,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { const notificationHubService = module.get( NotificationhubService, ); + const result = await service.uploadFinished( makeContext('trackingId', 'requestId'), authorExternalId, @@ -392,7 +414,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, 'http://blob/url/file.zip', 'file.zip', - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', ); @@ -411,6 +433,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000001'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -534,6 +560,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { userGroupId, // ルーティング先のユーザーグループIDを設定 ); + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -594,7 +623,7 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー - const { author_id: authorAuthorId } = await makeTestUser(source, { + await makeTestUser(source, { account_id: accountId, external_id: 'author-user-external-id', role: 'author', @@ -656,6 +685,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { userGroupId, // ルーティング先のユーザーグループIDを設定 ); + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -717,16 +749,17 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); // 音声ファイルの録音者のユーザー - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // 初期値のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000000'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -769,19 +802,375 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { // 自動ルーティングが行われていないことを確認 expect(resultCheckoutPermission.length).toEqual(0); }); + it('第五階層アカウントのストレージ使用量が閾値と同値の場合、メール送信が行われない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + // 閾値は、10GB * 0.8 = 8GBとなる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 3つの音声ファイルを事前作成+uploadFinishedで、合計8GBのストレージ使用量状態を作成 + for (let i = 0; i < 3; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000003'); + + const service = module.get(FilesService); + const spy = jest + .spyOn(service['sendGridService'], 'sendMail') + .mockImplementation(); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).not.toHaveBeenCalled(); + }); + it('第五階層アカウントのストレージ使用量が閾値+1byteの場合、U-118メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + // 閾値は、10GB * 0.8 = 8GBとなる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 3つの音声ファイルを事前作成+uploadFinishedで、合計8GB+1byteのストレージ使用量状態を作成 + for (let i = 0; i < 3; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000003'); + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU118') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize + 1, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); + it('第五階層アカウントのストレージ使用量が上限と同値の場合、U-118メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 4つの音声ファイルを事前作成+uploadFinishedで、合計10GBのストレージ使用量状態を作成 + for (let i = 0; i < 4; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000004'); + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU118') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); + it('第五階層アカウントのストレージ使用量が上限+1byteと同値の場合、U-119メール送信が行われる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + // アカウントを作成する + const { id: accountId } = ( + await makeTestAccount(source, { + tier: 5, + company_name: 'company1', + }) + ).account; + + // 第一階層アカウントを作成する + await makeTestAccount(source, { + tier: 1, + }); + + // 音声ファイルの録音者のユーザー + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + + // ライセンスを作成する。ライセンスが2つのため、5GB * 2 = 10GBの上限値となる。 + const reusableLicense = 2; + for (let i = 0; i < reusableLicense; i++) { + await createLicense( + source, + i + 1, + new Date(2037, 1, 1, 23, 59, 59), + accountId, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + } + + const fileSize = 2 * 1000 * 1000 * 1000; // 2GB + // 4つの音声ファイルを事前作成+uploadFinishedで、合計10GB+1byteのストレージ使用量状態を作成 + for (let i = 0; i < 4; i++) { + await createTask( + source, + accountId, + 'url', + 'test.zip', + 'InProgress', + undefined, + authorAuthorId ?? '', + undefined, + fileSize, + (i + 1).toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを更新 + await updateJobNumber(source, accountId, '00000004'); + + const service = module.get(FilesService); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendGridService'], 'sendMailWithU119') + .mockImplementation(); + overrideAdB2cService(service, { + getUsers: async () => [], + }); + const context = makeContext(`uuidv4`, 'xxx-xxx-xxx-xxx', 'requestId'); + await service.uploadFinished( + context, + authorExternalId, // API実行者のユーザーIDを設定 + 'http://blob/url/file.zip', + authorAuthorId ?? '', // 音声ファイルの情報には、録音者のAuthorIDが入る + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + fileSize + 1, + '01', + 'DS2', + 'comment', + 'worktypeId', + optionItemList, + false, + ); + + expect(spy).toHaveBeenCalledTimes(1); + }); it('日付フォーマットが不正な場合、エラーを返却する', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -819,16 +1208,13 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { it('オプションアイテムが10個ない場合、エラーを返却する', async () => { if (!source) fail(); const { id: accountId } = await makeTestSimpleAccount(source); - const { - external_id: authorExternalId, - id: authorUserId, - author_id: authorAuthorId, - } = await makeTestUser(source, { - account_id: accountId, - external_id: 'author-user-external-id', - role: 'author', - author_id: 'AUTHOR_ID', - }); + const { external_id: authorExternalId, author_id: authorAuthorId } = + await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -1011,9 +1397,11 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, `http://blob/url/file_${i}.zip`, `file_${i}.zip`, - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', + undefined, + undefined, i.toString().padStart(8, '0'), ); } @@ -1029,6 +1417,9 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + const module = await makeTestingModuleWithBlobAndNotification( source, blobParam, @@ -1135,13 +1526,18 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { accountId, `http://blob/url/file_${i}.zip`, `file_${i}.zip`, - '01', + 'Uploaded', typistUserId, authorAuthorId ?? '', + undefined, + undefined, i.toString().padStart(8, '0'), ); } + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + const module = await makeTestingModuleWithBlobAndNotification( source, blobParam, @@ -1251,6 +1647,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { '01', typistUserId, authorAuthorId ?? '', + undefined, + undefined, i.toString().padStart(8, '0'), ); } @@ -1265,6 +1663,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { 'Backup', false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); const module = await makeTestingModuleWithBlobAndNotification( source, @@ -1375,6 +1775,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { '01', typistUserId, authorAuthorId ?? '', + undefined, + undefined, i.toString().padStart(8, '0'), ); } @@ -1389,6 +1791,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { 'Backup', false, ); + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); const module = await makeTestingModuleWithBlobAndNotification( source, @@ -1486,6 +1890,10 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { ); // ワークフロータイピストを作成 await createWorkflowTypist(source, workflowId, typistUserId); + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); + const blobParam = makeBlobstorageServiceMockValue(); const notificationParam = makeDefaultNotificationhubServiceMockValue(); @@ -1499,6 +1907,8 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { '01', typistUserId, authorAuthorId ?? '', + undefined, + undefined, i.toString().padStart(8, '0'), ); } @@ -1555,6 +1965,455 @@ describe('タスク作成から自動ルーティング(DB使用)', () => { expect(resultCheckoutPermission.length).toEqual(1); expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); }); + + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(最新タスクが削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは00000001から00000010まで + for (let i = 1; i <= 10; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000010'); + } + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 最新のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '00000010'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(9); + // JobNumberが00000010のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '00000010'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000011'); + // job_numberテーブルが正しく更新されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000011'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(途中のタスクが削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは00000001から00000010まで + for (let i = 1; i <= 10; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '00000010'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000010'); + } + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 途中のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '00000005'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(9); + // JobNumberが00000005のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '00000005'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000011'); + // job_numberテーブルが正しく更新されているか確認 + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000011'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); + it('タスク作成時に採番されるジョブナンバーが常に最新タスクのジョブナンバー + 1となる(最新タスクのジョブナンバーが99999999で削除されている場合)', async () => { + if (!source) fail(); + const { id: accountId } = await makeTestSimpleAccount(source); + const { + external_id: authorExternalId, + id: authorUserId, + author_id: authorAuthorId, + } = await makeTestUser(source, { + account_id: accountId, + external_id: 'author-user-external-id', + role: 'author', + author_id: 'AUTHOR_ID', + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: accountId, + external_id: 'typist-user-external-id', + role: 'typist', + author_id: undefined, + }); + // ワークタイプを作成 + const { id: worktypeId, custom_worktype_id } = await createWorktype( + source, + accountId, + 'worktypeId', + ); + // テンプレートファイルを作成 + const { id: templateFileId } = await createTemplateFile( + source, + accountId, + 'templateFile', + 'http://blob/url/templateFile.zip', + ); + // ワークフローを作成 + const { id: workflowId } = await createWorkflow( + source, + accountId, + authorUserId, + worktypeId, + templateFileId, + ); + // ワークフロータイピストを作成 + await createWorkflowTypist(source, workflowId, typistUserId); + const blobParam = makeBlobstorageServiceMockValue(); + const notificationParam = makeDefaultNotificationhubServiceMockValue(); + + // タスクを10件作成, ジョブナンバーは99999989から99999999まで + for (let i = 99999989; i <= 99999999; i++) { + await createTask( + source, + accountId, + `http://blob/url/file_${i}.zip`, + `file_${i}.zip`, + 'Uploaded', + typistUserId, + authorAuthorId ?? '', + undefined, + undefined, + i.toString().padStart(8, '0'), + ); + } + + // 最新のジョブナンバーでjob_numberテーブルを作成 + await createJobNumber(source, accountId, '99999999'); + + { + // 初期データ確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(11); + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('99999999'); + } + + const module = await makeTestingModuleWithBlobAndNotification( + source, + blobParam, + notificationParam, + ); + if (!module) fail(); + const service = module.get(FilesService); + const taskService = module.get(TasksService); + const notificationHubService = module.get( + NotificationhubService, + ); + + // 最新のジョブナンバーのタスクを取得 + const latestTask = await getTaskFromJobNumber(source, '99999999'); + await taskService.deleteTask( + makeContext('trackingId', 'requestId'), + authorExternalId, + latestTask?.audio_file_id ?? 0, // 最新タスクのaudioFileId + ); + + { + // タスク削除確認 + const tasks = await getTasks(source, accountId); + expect(tasks.length).toEqual(10); + // JobNumberが99999999のタスクが存在しないことを確認 + expect( + tasks.find((task) => task.job_number === '99999999'), + ).toBeUndefined(); + } + + const result = await service.uploadFinished( + makeContext('trackingId', 'requestId'), + authorExternalId, + 'http://blob/url/file.zip', + authorAuthorId ?? '', + 'file.zip', + '11:22:33', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + '2023-05-26T11:22:33.444', + 256, + '01', + 'DS2', + 'comment', + custom_worktype_id, + optionItemList, + false, + ); + expect(result.jobNumber).toEqual('00000001'); + // job_numberテーブルが正しく更新されているか確認 + // 最新タスクのジョブナンバーが99999999の時、新規作成されたタスクのジョブナンバーは00000001になる + const jobNumber = await getJobNumber(source, accountId); + expect(jobNumber?.job_number).toEqual('00000001'); + + // 通知処理が想定通りの引数で呼ばれているか確認 + expect(notificationHubService.notify).toHaveBeenCalledWith( + makeContext('trackingId', 'requestId'), + [`user_${typistUserId}`], + { + authorId: 'AUTHOR_ID', + filename: 'file', + priority: 'High', + uploadedAt: '2023-05-26T11:22:33.444', + }, + ); + // 作成したタスクを取得 + const resultTask = await getTaskFromJobNumber(source, result.jobNumber); + // タスクのチェックアウト権限を取得 + const resultCheckoutPermission = await getCheckoutPermissions( + source, + resultTask?.id ?? 0, + ); + // タスクのテンプレートファイルIDを確認 + expect(resultTask?.template_file_id).toEqual(templateFileId); + // タスクのチェックアウト権限が想定通り(ワークフローで設定されている)のユーザーIDで作成されているか確認 + expect(resultCheckoutPermission.length).toEqual(1); + expect(resultCheckoutPermission[0].user_id).toEqual(typistUserId); + }); }); describe('音声ファイルダウンロードURL取得', () => { @@ -1571,6 +2430,8 @@ describe('音声ファイルダウンロードURL取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1879,6 +2740,8 @@ describe('テンプレートファイルダウンロードURL取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2142,6 +3005,8 @@ describe('publishTemplateFileUploadSas', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2257,6 +3122,8 @@ describe('templateUploadFinished', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2434,3 +3301,408 @@ const optionItemList = [ optionItemValue: 'value_10', }, ]; + +describe('fileRename', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ファイル名を変更できる(管理者)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + admin.external_id, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ファイル名を変更できる(Author)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: authorExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }); + + const context = makeContext(authorExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + 'AUTHOR_ID', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + authorExternalId, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ファイル名を変更できる(Typist)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: typistExternalId, id: typistId } = await makeTestUser( + source, + { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }, + ); + + const context = makeContext(typistExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + await createCheckoutPermissions(source, task.taskId, typistId); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + await service.fileRename( + context, + typistExternalId, + task.audioFileId, + newFileName, + ); + + //実行結果を確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(newFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + }); + it('ユーザーが管理者でなくRoleがNoneの場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: noneExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'none-user-external-id', + role: USER_ROLES.NONE, + }); + + const context = makeContext(noneExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + noneExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('Authorがファイル名変更をするときユーザーのAuthorIDとタスクのAuthorIDが異なる場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: authorExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_ID', + }); + + const context = makeContext(authorExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + 'AUTHOR_ID_XXX', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + authorExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('Typistがファイル名変更をするときユーザーがタスクのチェックアウト候補でない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const { external_id: typistExternalId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const context = makeContext(typistExternalId, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + } + + const newFileName = 'new.zip'; + + try { + await service.fileRename( + context, + typistExternalId, + task.audioFileId, + newFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021001')); + } else { + fail(); + } + } + }); + it('変更するファイル名がすでに存在する場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(FilesService); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const oldFileName = 'old.zip'; + + const task = await createTask( + source, + account.id, + 'https://blob.url/account-1', + oldFileName, + TASK_STATUS.UPLOADED, + undefined, + undefined, + undefined, + undefined, + '00000001', + ); + + const alreadyExistFileName = 'already.zip'; + const alreadyExistTask = await createTask( + source, + account.id, + 'https://blob.url/account-1', + alreadyExistFileName, + TASK_STATUS.UPLOADED, + undefined, + undefined, + undefined, + undefined, + '00000002', + ); + + // 事前にDBを確認 + { + const audioFile = await getAudioFile(source, task.audioFileId); + expect(audioFile?.file_name).toBe(oldFileName); + expect(audioFile?.raw_file_name).toBe(oldFileName); + + const alreadyExistAudioFile = await getAudioFile( + source, + alreadyExistTask.audioFileId, + ); + expect(alreadyExistAudioFile?.file_name).toBe(alreadyExistFileName); + } + + try { + await service.fileRename( + context, + admin.external_id, + task.audioFileId, + alreadyExistFileName, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toBe(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E021002')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/files/files.service.ts b/dictation_server/src/features/files/files.service.ts index 1f9c042..3d0084a 100644 --- a/dictation_server/src/features/files/files.service.ts +++ b/dictation_server/src/features/files/files.service.ts @@ -6,6 +6,7 @@ import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.servi import { AudioOptionItem, AudioUploadFinishedResponse } from './types/types'; import { OPTION_ITEM_NUM, + STORAGE_WARNING_THRESHOLD_PERCENT, TASK_STATUS, TIERS, USER_LICENSE_STATUS, @@ -37,11 +38,24 @@ import { LicenseNotAllocatedError, } from '../../repositories/licenses/errors/types'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; +import { DateWithZeroTime } from '../licenses/types/types'; +import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; +import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; +import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; +import { AudioFilesRepositoryService } from '../../repositories/audio_files/audio_files.repository.service'; +import { + CheckoutPermissionNotFoundError, + FileNameAlreadyExistsError, + RoleNotMatchError, +} from '../../repositories/audio_files/errors/types'; @Injectable() export class FilesService { private readonly logger = new Logger(FilesService.name); constructor( + private readonly accountsRepository: AccountsRepositoryService, + private readonly adB2cService: AdB2cService, private readonly usersRepository: UsersRepositoryService, private readonly tasksRepository: TasksRepositoryService, private readonly tasksRepositoryService: TasksRepositoryService, @@ -50,6 +64,8 @@ export class FilesService { private readonly userGroupsRepositoryService: UserGroupsRepositoryService, private readonly notificationhubService: NotificationhubService, private readonly licensesRepository: LicensesRepositoryService, + private readonly sendGridService: SendGridService, + private readonly audioFilesRepositoryService: AudioFilesRepositoryService, ) {} /** @@ -211,11 +227,20 @@ export class FilesService { ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.uploadFinished.name}`, + ); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, ); } + + // 第五階層アカウントはストレージ使用量超過チェックをする。順番は自動ルーティングの前後どちらでも構わない。 + if (user.account?.tier === TIERS.TIER5) { + await this.checkAndAlertStorageUsage(context, user.account_id); + } + try { // ルーティング設定に従い、チェックアウト権限を付与する const { typistGroupIds, typistIds } = @@ -271,6 +296,135 @@ export class FilesService { } } + /** + * ストレージ使用量を確認し、上限または閾値を超えていた場合に通知を行う + * @param context + * @param accountId 確認対象のアカウントID + * @returns + */ + private async checkAndAlertStorageUsage( + context: Context, + accountId: number, + ): Promise { + try { + const currentDate = new DateWithZeroTime(); + const { size, used } = await this.licensesRepository.getStorageInfo( + context, + accountId, + currentDate, + ); + const storageShresholdSize = + (size * STORAGE_WARNING_THRESHOLD_PERCENT) / 100; + + if (used > size) { + this.logger.log( + `[${context.getTrackingId()}] ${ + this.checkAndAlertStorageUsage.name + } | Storage usage is over limit. accountId=${accountId}, size=${size}, used=${used}`, + ); + const tier1AccountId = ( + await this.accountsRepository.findTier1Account(context) + ).id; + const tire1AdminMails = ( + await this.getAccountInformation(context, tier1AccountId) + ).adminEmails; + + const dealer = await this.accountsRepository.findParentAccount( + context, + accountId, + ); + const dealerName: string | null = dealer?.company_name ?? null; + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + await this.sendGridService.sendMailWithU119( + context, + adminEmails, + companyName, + dealerName, + tire1AdminMails, + ); + return; + } + + if (used > storageShresholdSize) { + this.logger.log( + `[${context.getTrackingId()}] ${ + this.checkAndAlertStorageUsage.name + } | Storage usage is over shresholdSize. accountId=${accountId}, size=${size}, storageShresholdSize=${storageShresholdSize}`, + ); + const dealer = await this.accountsRepository.findParentAccount( + context, + accountId, + ); + const dealerName: string | null = dealer?.company_name ?? null; + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + await this.sendGridService.sendMailWithU118( + context, + adminEmails, + companyName, + dealerName, + ); + + return; + } + } catch (error) { + // uploadする度にストレージ使用量チェックするため、一連の処理に失敗しても例外は握りつぶす + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + } + } + + /** + * アカウントIDを指定して、アカウント情報と管理者情報を取得する + * @param context + * @param accountId 対象アカウントID + * @returns 企業名/管理者メールアドレス + */ + private async getAccountInformation( + context: Context, + accountId: number, + ): Promise<{ + companyName: string; + adminEmails: string[]; + }> { + // アカウントIDから企業名を取得する + const { company_name } = await this.accountsRepository.findAccountById( + context, + accountId, + ); + + // 管理者一覧を取得 + const admins = await this.usersRepository.findAdminUsers( + context, + accountId, + ); + const adminExternalIDs = admins.map((x) => x.external_id); + + // ADB2Cから管理者IDを元にメールアドレスを取得する + const usersInfo = await this.adB2cService.getUsers( + context, + adminExternalIDs, + ); + + // 生のAzure AD B2Cのユーザー情報からメールアドレスを抽出する + const adminEmails = usersInfo.map((x) => { + const { emailAddress } = getUserNameAndMailAddress(x); + if (emailAddress == null) { + throw new Error('admin email-address is not found'); + } + return emailAddress; + }); + + return { + companyName: company_name, + adminEmails: adminEmails, + }; + } + /** * Publishs upload sas * @param companyName @@ -457,7 +611,7 @@ export class FilesService { } } - const filePath = `${file.file_name}`; + const filePath = `${file.raw_file_name}`; const isFileExist = await this.blobStorageService.fileExists( context, @@ -469,7 +623,7 @@ export class FilesService { if (!isFileExist) { this.logger.log(`[${context.getTrackingId()}] filePath:${filePath}`); throw new AudioFileNotFoundError( - `Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${file.file_name}`, + `Audio file is not exists in blob storage. audio_file_id:${audioFileId}, url:${file.url}, fileName:${filePath}`, ); } @@ -764,4 +918,73 @@ export class FilesService { ); } } + + /** + * 音声ファイルの表示ファイル名を変更する + * @param context + * @param externalId + * @param audioFileId + * @param fileName + * @returns rename + */ + async fileRename( + context: Context, + externalId: string, + audioFileId: number, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.fileRename.name + } | params: { externalId: ${externalId}, audioFileId: ${audioFileId}, fileName: ${fileName} };`, + ); + + try { + // ユーザー取得 + const { account_id: accountId, id: userId } = + await this.usersRepository.findUserByExternalId(context, externalId); + + // 音声ファイルの表示ファイル名を変更 + await this.audioFilesRepositoryService.renameAudioFile( + context, + accountId, + userId, + audioFileId, + fileName, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + + if (e instanceof Error) { + switch (e.constructor) { + case TasksNotFoundError: + case RoleNotMatchError: + case AuthorUserNotMatchError: + case CheckoutPermissionNotFoundError: + throw new HttpException( + makeErrorResponse('E021001'), + HttpStatus.BAD_REQUEST, + ); + case FileNameAlreadyExistsError: + throw new HttpException( + makeErrorResponse('E021002'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.fileRename.name}`, + ); + } + } } diff --git a/dictation_server/src/features/files/test/files.service.mock.ts b/dictation_server/src/features/files/test/files.service.mock.ts index 8e9ca51..1505888 100644 --- a/dictation_server/src/features/files/test/files.service.mock.ts +++ b/dictation_server/src/features/files/test/files.service.mock.ts @@ -8,6 +8,7 @@ import { Task } from '../../../repositories/tasks/entity/task.entity'; import { TemplateFilesRepositoryService } from '../../../repositories/template_files/template_files.repository.service'; import { NotificationhubService } from '../../../gateways/notificationhub/notificationhub.service'; import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; +import { FILE_RETENTION_DAYS_DEFAULT } from '../../../constants'; export type BlobstorageServiceMockValue = { createContainer: void | Error; @@ -160,6 +161,8 @@ export const makeDefaultUsersRepositoryMockValue = tier: 5, country: '', delegation_permission: true, + auto_file_delete: false, + file_retention_days: FILE_RETENTION_DAYS_DEFAULT, locked: false, company_name: '', verified: true, diff --git a/dictation_server/src/features/files/test/utility.ts b/dictation_server/src/features/files/test/utility.ts index 25f9397..5b96e99 100644 --- a/dictation_server/src/features/files/test/utility.ts +++ b/dictation_server/src/features/files/test/utility.ts @@ -44,6 +44,7 @@ import { import { UserGroup } from '../../../repositories/user_groups/entity/user_group.entity'; import { UserGroupMember } from '../../../repositories/user_groups/entity/user_group_member.entity'; import { License } from '../../../repositories/licenses/entity/license.entity'; +import { JobNumber } from '../../../repositories/job_number/entity/job_number.entity'; export const createTask = async ( datasource: DataSource, @@ -53,22 +54,25 @@ export const createTask = async ( status: string, typist_user_id?: number | undefined, author_id?: string | undefined, - job_number?: string | undefined, -): Promise<{ audioFileId: number }> => { + owner_user_id?: number | undefined, + fileSize?: number | undefined, + jobNumber?: string | undefined, +): Promise<{ audioFileId: number; taskId: number }> => { const { identifiers: audioFileIdentifiers } = await datasource .getRepository(AudioFile) .insert({ account_id: account_id, - owner_user_id: 1, + owner_user_id: owner_user_id ?? 1, url: url, file_name: fileName, + raw_file_name: fileName, author_id: author_id ?? 'DEFAULT_ID', work_type_id: 'work_type_id', started_at: new Date(), duration: '100000', finished_at: new Date(), uploaded_at: new Date(), - file_size: 10000, + file_size: fileSize ?? 10000, priority: '00', audio_format: 'audio_format', is_encrypted: true, @@ -87,20 +91,23 @@ export const createTask = async ( }); const templateFile = templateFileIdentifiers.pop() as TemplateFile; - await datasource.getRepository(Task).insert({ - job_number: job_number ?? '00000001', - account_id: account_id, - is_job_number_enabled: true, - audio_file_id: audioFile.id, - template_file_id: templateFile.id, - typist_user_id: typist_user_id, - status: status, - priority: '01', - started_at: new Date().toISOString(), - created_at: new Date(), - }); + const { identifiers: TaskIdentifiers } = await datasource + .getRepository(Task) + .insert({ + job_number: jobNumber ?? '00000001', + account_id: account_id, + is_job_number_enabled: true, + audio_file_id: audioFile.id, + template_file_id: templateFile.id, + typist_user_id: typist_user_id, + status: status, + priority: '01', + started_at: new Date().toISOString(), + created_at: new Date(), + }); + const task = TaskIdentifiers.pop() as Task; - return { audioFileId: audioFile.id }; + return { audioFileId: audioFile.id, taskId: task.id }; }; export const getTaskFromJobNumber = async ( diff --git a/dictation_server/src/features/files/types/types.ts b/dictation_server/src/features/files/types/types.ts index 3545a20..57bdb41 100644 --- a/dictation_server/src/features/files/types/types.ts +++ b/dictation_server/src/features/files/types/types.ts @@ -8,6 +8,7 @@ import { IsInt, IsNotEmpty, IsNumberString, + IsString, MaxLength, Min, MinLength, @@ -141,3 +142,18 @@ export class TemplateUploadFinishedRequest { } export class TemplateUploadFinishedReqponse {} + +export class FileRenameRequest { + @ApiProperty({ description: 'ファイル名変更対象の音声ファイルID' }) + @Type(() => Number) + @Min(1) + @IsInt() + audioFileId: number; + @ApiProperty({ description: '変更するファイル名' }) + @IsString() + @MaxLength(64) + @MinLength(1) + fileName: string; +} + +export class FileRenameResponse {} diff --git a/dictation_server/src/features/licenses/licenses.service.spec.ts b/dictation_server/src/features/licenses/licenses.service.spec.ts index 69b6530..37912c3 100644 --- a/dictation_server/src/features/licenses/licenses.service.spec.ts +++ b/dictation_server/src/features/licenses/licenses.service.spec.ts @@ -25,6 +25,7 @@ import { } from '../../constants'; import { makeHierarchicalAccounts, + makeTestAccount, makeTestSimpleAccount, makeTestUser, } from '../../common/test/utility'; @@ -34,6 +35,7 @@ import { overrideSendgridService, } from '../../common/test/overrides'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('ライセンス注文', () => { let source: DataSource | null = null; @@ -49,6 +51,8 @@ describe('ライセンス注文', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -222,6 +226,8 @@ describe('カードライセンス発行', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -312,6 +318,8 @@ describe('カードライセンスを取り込む', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -380,8 +388,12 @@ describe('カードライセンスを取り込む', () => { if (!source) fail(); const module = await makeTestingModule(source); if (!module) fail(); + // 明日の日付を取得 + // ミリ秒以下は切り捨てる + const tommorow = new Date(); + tommorow.setDate(tommorow.getDate() + 1); + tommorow.setMilliseconds(0); - const now = new Date(); const { id: accountId } = await makeTestSimpleAccount(source); const { external_id: externalId } = await makeTestUser(source, { account_id: accountId, @@ -395,7 +407,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 1, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -421,7 +433,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 3, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -434,7 +446,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 4, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -460,7 +472,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 6, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.ALLOCATED, @@ -473,7 +485,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 7, - new Date(now.getTime() + 60 * 60 * 1000 * 2), + new Date(tommorow.getTime() + 60 * 60 * 1000 * 2), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.DELETED, @@ -486,7 +498,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 8, - new Date(now.getTime() + 60 * 60 * 1000), + new Date(tommorow.getTime() + 60 * 60 * 1000), accountId + 1, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -499,7 +511,7 @@ describe('カードライセンスを取り込む', () => { await createLicense( source, 9, - new Date(now.getTime() - 60 * 60 * 1000 * 24), + new Date(tommorow.getTime() - 60 * 60 * 1000 * 24), accountId, LICENSE_TYPE.NORMAL, LICENSE_ALLOCATED_STATUS.UNALLOCATED, @@ -657,6 +669,8 @@ describe('ライセンス割り当て', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1302,6 +1316,8 @@ describe('ライセンス割り当て解除', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1471,6 +1487,8 @@ describe('ライセンス注文キャンセル', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1597,3 +1615,397 @@ describe('ライセンス注文キャンセル', () => { ); }); }); + +describe('割り当て可能なライセンス取得', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('割り当て可能なライセンスを取得できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が当日のライセンスを1つ、有効期限が翌日のライセンスを1つ、有効期限が翌々日のライセンスを1つ作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const today = new Date(); + today.setMilliseconds(0); + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setMilliseconds(0); + const dayAfterTomorrow = new Date(); + dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 2); + dayAfterTomorrow.setMilliseconds(0); + await createLicense( + source, + 1, + today, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + await createLicense( + source, + 2, + tomorrow, + account.id, + LICENSE_TYPE.CARD, + LICENSE_ALLOCATED_STATUS.REUSABLE, + null, + null, + null, + null, + ); + await createLicense( + source, + 3, + dayAfterTomorrow, + account.id, + LICENSE_TYPE.TRIAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const license1 = await selectLicense(source, 1); + expect(license1.license?.id).toBe(1); + expect(license1.license?.expiry_date).toEqual(today); + expect(license1.license?.allocated_user_id).toBe(null); + expect(license1.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license1.license?.account_id).toBe(account.id); + expect(license1.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license2 = await selectLicense(source, 2); + expect(license2.license?.id).toBe(2); + expect(license2.license?.expiry_date).toEqual(tomorrow); + expect(license2.license?.allocated_user_id).toBe(null); + expect(license2.license?.status).toBe(LICENSE_ALLOCATED_STATUS.REUSABLE); + expect(license2.license?.account_id).toBe(account.id); + expect(license2.license?.type).toBe(LICENSE_TYPE.CARD); + const license3 = await selectLicense(source, 3); + expect(license3.license?.id).toBe(3); + expect(license3.license?.expiry_date).toEqual(dayAfterTomorrow); + expect(license3.license?.allocated_user_id).toBe(null); + expect(license3.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license3.license?.account_id).toBe(account.id); + expect(license3.license?.type).toBe(LICENSE_TYPE.TRIAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + + // 有効期限が当日のライセンスは取得されない + // 有効期限が長い順に取得される + expect(response.allocatableLicenses.length).toBe(2); + expect(response.allocatableLicenses[0].licenseId).toBe(3); + expect(response.allocatableLicenses[0].expiryDate).toEqual( + dayAfterTomorrow, + ); + expect(response.allocatableLicenses[1].licenseId).toBe(2); + expect(response.allocatableLicenses[1].expiryDate).toEqual(tomorrow); + }); + + it('割り当て可能なライセンスが存在しない場合、空の配列を返却する', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が当日のライセンスを3つ、有効期限が昨日のライセンスを1つ作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const today = new Date(); + today.setMilliseconds(0); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setMilliseconds(0); + for (let i = 1; i <= 3; i++) { + await createLicense( + source, + i, + today, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + } + await createLicense( + source, + 4, + yesterday, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const license1 = await selectLicense(source, 1); + expect(license1.license?.id).toBe(1); + expect(license1.license?.expiry_date).toEqual(today); + expect(license1.license?.allocated_user_id).toBe(null); + expect(license1.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license1.license?.account_id).toBe(account.id); + expect(license1.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license2 = await selectLicense(source, 2); + expect(license2.license?.id).toBe(2); + expect(license2.license?.expiry_date).toEqual(today); + expect(license2.license?.allocated_user_id).toBe(null); + expect(license2.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license2.license?.account_id).toBe(account.id); + expect(license2.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license3 = await selectLicense(source, 3); + expect(license3.license?.id).toBe(3); + expect(license3.license?.expiry_date).toEqual(today); + expect(license3.license?.allocated_user_id).toBe(null); + expect(license3.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license3.license?.account_id).toBe(account.id); + expect(license3.license?.type).toBe(LICENSE_TYPE.NORMAL); + const license4 = await selectLicense(source, 4); + expect(license4.license?.id).toBe(4); + expect(license4.license?.expiry_date).toEqual(yesterday); + expect(license4.license?.allocated_user_id).toBe(null); + expect(license4.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license4.license?.account_id).toBe(account.id); + expect(license4.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 有効期限が当日のライセンスは取得されない + // 有効期限が切れているライセンスは取得されない + expect(response.allocatableLicenses.length).toBe(0); + expect(response.allocatableLicenses).toEqual([]); + }); + + it('割り当て可能なライセンスを100件取得できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // 有効期限が30日後のライセンスを100件作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const date = new Date(); + date.setDate(date.getDate() + 30); + date.setMilliseconds(0); + for (let i = 1; i <= 100; i++) { + await createLicense( + source, + i, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + } + // ライセンス作成したデータを確認 + for (let i = 1; i <= 100; i++) { + const license = await selectLicense(source, i); + expect(license.license?.id).toBe(i); + expect(license.license?.expiry_date).toEqual(date); + expect(license.license?.allocated_user_id).toBe(null); + expect(license.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license.license?.account_id).toBe(account.id); + expect(license.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 100件取得できる + expect(response.allocatableLicenses.length).toBe(100); + }); + it('既に割り当てられているライセンスは取得されない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const { account, admin } = await makeTestAccount(source, { + company_name: 'AAA', + tier: 5, + }); + // ライセンスを作成 + // ライセンスを5件作成(10日後から1日ずつ有効期限を設定) + // 有効期限が設定されていないライセンス(新規ライセンス)を1件作成 + // 既に割り当てられているライセンスを1件作成 + // ミリ秒以下は切り捨てる DBで扱っていないため + const date = new Date(); + date.setMinutes(0); + date.setSeconds(0); + date.setDate(date.getDate() + 10); + date.setMilliseconds(0); + for (let i = 1; i <= 5; i++) { + await createLicense( + source, + i, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + admin.id, + null, + null, + null, + ); + date.setDate(date.getDate() + 1); + } + // 新規ライセンス + await createLicense( + source, + 6, + null, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + null, + null, + null, + null, + ); + // 既に割り当てられているライセンス + await createLicense( + source, + 7, + date, + account.id, + LICENSE_TYPE.NORMAL, + LICENSE_ALLOCATED_STATUS.ALLOCATED, + admin.id, + null, + null, + null, + ); + // ライセンス作成したデータを確認 + { + const date = new Date(); + date.setMinutes(0); + date.setSeconds(0); + date.setDate(date.getDate() + 10); + date.setMilliseconds(0); + for (let i = 1; i <= 5; i++) { + const license = await selectLicense(source, i); + expect(license.license?.id).toBe(i); + expect(license.license?.expiry_date).toEqual(date); + expect(license.license?.allocated_user_id).toBe(admin.id); + expect(license.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + expect(license.license?.account_id).toBe(account.id); + expect(license.license?.type).toBe(LICENSE_TYPE.NORMAL); + date.setDate(date.getDate() + 1); + } + const newLicense = await selectLicense(source, 6); + expect(newLicense.license?.id).toBe(6); + expect(newLicense.license?.expiry_date).toBe(null); + expect(newLicense.license?.allocated_user_id).toBe(null); + expect(newLicense.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.UNALLOCATED, + ); + const allocatedLicense = await selectLicense(source, 7); + expect(allocatedLicense.license?.id).toBe(7); + expect(allocatedLicense.license?.expiry_date).toEqual(date); + expect(allocatedLicense.license?.allocated_user_id).toBe(admin.id); + expect(allocatedLicense.license?.status).toBe( + LICENSE_ALLOCATED_STATUS.ALLOCATED, + ); + expect(allocatedLicense.license?.account_id).toBe(account.id); + expect(allocatedLicense.license?.type).toBe(LICENSE_TYPE.NORMAL); + } + + const service = module.get(LicensesService); + const context = makeContext('trackingId', 'requestId'); + const response = await service.getAllocatableLicenses( + context, + admin.external_id, + ); + // 既に割り当てられているライセンスは取得されない + // 新規ライセンスは取得される + // 有効期限が長い順に取得される + expect(response.allocatableLicenses.length).toBe(6); + expect(response.allocatableLicenses[0].licenseId).toBe(6); + expect(response.allocatableLicenses[0].expiryDate).toBe(undefined); + expect(response.allocatableLicenses[1].licenseId).toBe(5); // 有効期限が最も長い + expect(response.allocatableLicenses[2].licenseId).toBe(4); + expect(response.allocatableLicenses[3].licenseId).toBe(3); + expect(response.allocatableLicenses[4].licenseId).toBe(2); + expect(response.allocatableLicenses[5].licenseId).toBe(1); + }); +}); diff --git a/dictation_server/src/features/notification/notification.controller.spec.ts b/dictation_server/src/features/notification/notification.controller.spec.ts index bbf71a7..7f95d10 100644 --- a/dictation_server/src/features/notification/notification.controller.spec.ts +++ b/dictation_server/src/features/notification/notification.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notification.service'; import { ConfigModule } from '@nestjs/config'; +import { RegisterRequest } from './types/types'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; describe('NotificationController', () => { let controller: NotificationController; @@ -28,3 +31,57 @@ describe('NotificationController', () => { expect(controller).toBeDefined(); }); }); + +describe('valdation register', () => { + it('有効なリクエストが成功する(wns)', async () => { + const request = new RegisterRequest(); + request.pns = 'wns'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('有効なリクエストが成功する(apn)', async () => { + const request = new RegisterRequest(); + request.pns = 'apns'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('pnsが不正(wns or apns以外)な場合、リクエストが失敗する', async () => { + const request = new RegisterRequest(); + request.pns = 'invalid'; + request.handler = 'test'; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('handlerが空文字の場合、リクエストが失敗する', async () => { + const request = new RegisterRequest(); + request.pns = 'apns'; + request.handler = ''; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('handlerが文字列でない場合、リクエストが失敗する', async () => { + const request = { + pns: 'apns', + handler: 123, + }; + + const valdationObject = plainToClass(RegisterRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); +}); diff --git a/dictation_server/src/features/tasks/tasks.controller.spec.ts b/dictation_server/src/features/tasks/tasks.controller.spec.ts index 72b8ac5..c753ab4 100644 --- a/dictation_server/src/features/tasks/tasks.controller.spec.ts +++ b/dictation_server/src/features/tasks/tasks.controller.spec.ts @@ -2,6 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { TasksController } from './tasks.controller'; import { TasksService } from './tasks.service'; import { ConfigModule } from '@nestjs/config'; +import { TasksRequest } from './types/types'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; describe('TasksController', () => { let controller: TasksController; @@ -27,4 +30,89 @@ describe('TasksController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation getTasks', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new TasksRequest(); + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('設定可能な全てのパラメータが指定されている場合、リクエストが成功する', async () => { + const request = new TasksRequest(); + request.limit = 1; + request.offset = 1; + request.status = 'Uploaded'; + request.direction = 'ASC'; + request.paramName = 'JOB_NUMBER'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + it('limitが0より小さい場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.limit = -1; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('limitが文字列の場合、リクエストが失敗する', async () => { + const request = { limit: 'test' }; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('offsetが0より小さい場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.offset = -1; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('offsetが文字列の場合、リクエストが失敗する', async () => { + const request = { offset: 'test' }; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('statusがタスクのステータス文字列以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.status = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('directionがASC,DESC以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.direction = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('paramNameがTASK_LIST_SORTABLE_ATTRIBUTES以外の場合、リクエストが失敗する', async () => { + const request = new TasksRequest(); + request.paramName = 'test'; + + const valdationObject = plainToClass(TasksRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/tasks/tasks.controller.ts b/dictation_server/src/features/tasks/tasks.controller.ts index 18f3772..7855c34 100644 --- a/dictation_server/src/features/tasks/tasks.controller.ts +++ b/dictation_server/src/features/tasks/tasks.controller.ts @@ -29,6 +29,8 @@ import { ChangeStatusResponse, PostCheckoutPermissionRequest, PostCheckoutPermissionResponse, + PostDeleteTaskRequest, + PostDeleteTaskResponse, TasksRequest, TasksResponse, } from './types/types'; @@ -752,4 +754,81 @@ export class TasksController { return {}; } + + @Post(':audioFileId/delete') + @ApiResponse({ + status: HttpStatus.OK, + type: PostDeleteTaskResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteTask', + description: '指定した文字起こしタスクを削除します。', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN, USER_ROLES.AUTHOR] }), + ) + async deleteTask( + @Req() req: Request, + @Param() params: PostDeleteTaskRequest, + ): Promise { + const { audioFileId } = params; + + // AuthGuardでチェック済みなのでここでのアクセストークンチェックはしない + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.taskService.deleteTask(context, userId, audioFileId); + return {}; + } } diff --git a/dictation_server/src/features/tasks/tasks.module.ts b/dictation_server/src/features/tasks/tasks.module.ts index b95229b..67583a2 100644 --- a/dictation_server/src/features/tasks/tasks.module.ts +++ b/dictation_server/src/features/tasks/tasks.module.ts @@ -8,6 +8,7 @@ import { UserGroupsRepositoryModule } from '../../repositories/user_groups/user_ import { NotificationhubModule } from '../../gateways/notificationhub/notificationhub.module'; import { SendGridModule } from '../../gateways/sendgrid/sendgrid.module'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.repository.module'; @Module({ @@ -19,6 +20,7 @@ import { LicensesRepositoryModule } from '../../repositories/licenses/licenses.r AdB2cModule, NotificationhubModule, SendGridModule, + BlobstorageModule, LicensesRepositoryModule, ], providers: [TasksService], diff --git a/dictation_server/src/features/tasks/tasks.service.spec.ts b/dictation_server/src/features/tasks/tasks.service.spec.ts index 97f4ee2..cb59c38 100644 --- a/dictation_server/src/features/tasks/tasks.service.spec.ts +++ b/dictation_server/src/features/tasks/tasks.service.spec.ts @@ -15,6 +15,8 @@ import { createCheckoutPermissions, createTask, createUserGroup, + getAudioFile, + getAudioOptionItems, getCheckoutPermissions, getTask, makeTaskTestingModuleWithNotificaiton, @@ -22,9 +24,11 @@ import { import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service'; import { makeContext } from '../../common/log'; import { + createSortCriteria, makeTestAccount, makeTestSimpleAccount, makeTestUser, + updateSortCriteria, } from '../../common/test/utility'; import { ADMIN_ROLES, @@ -34,7 +38,6 @@ import { USER_ROLES, } from '../../constants'; import { makeTestingModule } from '../../common/test/modules'; -import { createSortCriteria } from '../users/test/utility'; import { createWorktype } from '../accounts/test/utility'; import { createWorkflow, @@ -44,10 +47,13 @@ import { createTemplateFile } from '../templates/test/utility'; import { NotificationhubService } from '../../gateways/notificationhub/notificationhub.service'; import { Roles } from '../../common/types/role'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; +import { overrideBlobstorageService } from '../../common/test/overrides'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { truncateAllTable } from '../../common/test/init'; import { makeDefaultLicensesRepositoryMockValue } from '../accounts/test/accounts.service.mock'; import { DateWithZeroTime } from '../licenses/types/types'; import { createLicense } from '../licenses/test/utility'; +import { TestLogger } from '../../common/test/logger'; describe('TasksService', () => { it('タスク一覧を取得できる(admin)', async () => { @@ -99,6 +105,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -246,6 +253,7 @@ describe('TasksService', () => { owner_user_id: 1, url: 'test/test.zip', file_name: 'test.zip', + raw_file_name: 'test.zip', author_id: 'AUTHOR', work_type_id: 'WorkType', started_at: new Date('2023-01-01T01:01:01.000'), @@ -359,6 +367,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -494,6 +503,7 @@ describe('TasksService', () => { authorId: 'AUTHOR', comment: 'comment', fileName: 'test.zip', + rawFileName: 'test.zip', fileSize: 123000, isEncrypted: true, jobNumber: '00000001', @@ -634,6 +644,8 @@ describe('TasksService', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1029,6 +1041,8 @@ describe('changeCheckoutPermission', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1671,6 +1685,8 @@ describe('checkout', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2442,6 +2458,8 @@ describe('checkin', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2657,6 +2675,8 @@ describe('suspend', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2868,6 +2888,8 @@ describe('cancel', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3496,6 +3518,8 @@ describe('backup', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3802,6 +3826,8 @@ describe('getNextTask', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -3838,7 +3864,7 @@ describe('getNextTask', () => { role: USER_ROLES.TYPIST, }); - await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + await updateSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); const { taskId: taskId1 } = await createTask( source, @@ -3982,7 +4008,7 @@ describe('getNextTask', () => { role: USER_ROLES.TYPIST, }); - await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); + await updateSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC'); const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask( source, @@ -4276,3 +4302,703 @@ describe('getNextTask', () => { } }); }); + +describe('deleteTask', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('管理者として、アカウント内のタスクを削除できる(タスクのStatusがUploaded)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, admin.external_id, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'y.zip', + ); + } + }); + it('Authorとして、自身が追加したタスクを削除できる(タスクのStatusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.FINISHED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.FINISHED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, authorExternalId, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'y.zip', + ); + } + }); + it('Authorとして、自身が追加したタスクを削除できる(タスクのStatusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.BACKUP, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.BACKUP); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const blobStorageService = + module.get(BlobstorageService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + await service.deleteTask(context, authorExternalId, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'y.zip', + ); + } + }); + it('ステータスがInProgressのタスクを削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.IN_PROGRESS, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.IN_PROGRESS); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } + }); + it('ステータスがPendingのタスクを削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.PENDING, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.PENDING); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010601')); + } else { + fail(); + } + } + }); + it('Authorが自身が作成したタスク以外を削除しようとした場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId1 = 'AUTHOR_ID1'; + const authorId2 = 'AUTHOR_ID2'; + + const { id: authorUserId1 } = await makeTestUser(source, { + account_id: account.id, + author_id: authorId1, + external_id: 'author-user-external-id1', + role: USER_ROLES.AUTHOR, + }); + const { external_id: authorExternalId2 } = await makeTestUser(source, { + account_id: account.id, + author_id: authorId2, + external_id: 'author-user-external-id2', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId1, + authorId1, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId1); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId2, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId2, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010602')); + } else { + fail(); + } + } + }); + it('削除対象タスクが存在しない場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { admin } = await makeTestAccount(source, { tier: 5 }); + + const service = module.get(TasksService); + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, admin.external_id, 1); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010603')); + } else { + fail(); + } + } + }); + it('タスクのDB削除に失敗した場合、エラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId, external_id: authorExternalId } = + await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + const context = makeContext(authorExternalId, 'requestId'); + + // DBアクセスに失敗するようにする + const tasksRepositoryService = module.get( + TasksRepositoryService, + ); + tasksRepositoryService.deleteTask = jest + .fn() + .mockRejectedValue('DB failed'); + + overrideBlobstorageService(service, { + // eslint-disable-next-line @typescript-eslint/no-empty-function + deleteFile: async () => {}, + }); + + try { + await service.deleteTask(context, authorExternalId, audioFileId); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); + it('blobストレージからの音声ファイル削除に失敗した場合でも、エラーとならないこと', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const authorId = 'AUTHOR_ID'; + const { id: authorUserId } = await makeTestUser(source, { + account_id: account.id, + author_id: 'AUTHOR_ID', + external_id: 'author-user-external-id', + role: USER_ROLES.AUTHOR, + }); + const { id: typistUserId } = await makeTestUser(source, { + account_id: account.id, + external_id: 'typist-user-external-id', + role: USER_ROLES.TYPIST, + }); + + const { taskId, audioFileId } = await createTask( + source, + account.id, + authorUserId, + authorId, + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await createCheckoutPermissions(source, taskId, typistUserId); + + // 作成したデータを確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task?.id).toBe(taskId); + expect(task?.status).toBe(TASK_STATUS.UPLOADED); + expect(task?.audio_file_id).toBe(audioFileId); + + expect(audioFile?.id).toBe(audioFileId); + expect(audioFile?.file_name).toBe('x.zip'); + expect(audioFile?.author_id).toBe(authorId); + + expect(checkoutPermissions.length).toBe(1); + expect(checkoutPermissions[0].user_id).toBe(typistUserId); + + expect(optionItems.length).toBe(10); + } + + const service = module.get(TasksService); + + const context = makeContext(admin.external_id, 'requestId'); + + overrideBlobstorageService(service, { + deleteFile: async () => { + throw new Error('blob failed'); + }, + }); + + await service.deleteTask(context, admin.external_id, audioFileId); + + // 実行結果が正しいか確認 + { + const task = await getTask(source, taskId); + const audioFile = await getAudioFile(source, audioFileId); + const checkoutPermissions = await getCheckoutPermissions(source, taskId); + const optionItems = await getAudioOptionItems(source, taskId); + + expect(task).toBe(null); + expect(audioFile).toBe(null); + expect(checkoutPermissions.length).toBe(0); + expect(optionItems.length).toBe(0); + } + }); +}); diff --git a/dictation_server/src/features/tasks/tasks.service.ts b/dictation_server/src/features/tasks/tasks.service.ts index 2e19af4..fd335d4 100644 --- a/dictation_server/src/features/tasks/tasks.service.ts +++ b/dictation_server/src/features/tasks/tasks.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service'; -import { Assignee, Task } from './types/types'; +import { Assignee, PostDeleteTaskRequest, Task } from './types/types'; import { Task as TaskEntity } from '../../repositories/tasks/entity/task.entity'; import { createTasks } from './types/convert'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; @@ -11,6 +11,7 @@ import { } from '../../common/types/sort'; import { ADMIN_ROLES, + MANUAL_RECOVERY_REQUIRED, TASK_STATUS, TIERS, USER_LICENSE_STATUS, @@ -42,6 +43,7 @@ import { User } from '../../repositories/users/entity/user.entity'; import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; import { LicenseExpiredError, @@ -60,6 +62,7 @@ export class TasksService { private readonly adB2cService: AdB2cService, private readonly sendgridService: SendGridService, private readonly notificationhubService: NotificationhubService, + private readonly blobStorageService: BlobstorageService, private readonly licensesRepository: LicensesRepositoryService, ) {} @@ -891,6 +894,107 @@ export class TasksService { } } + /** + * 指定した音声ファイルに紐づくタスクを削除します + * @param context + * @param externalId 実行ユーザーの外部ID + * @param audioFileId 削除対象のタスクのaudio_file_id + * @returns task + */ + async deleteTask( + context: Context, + externalId: string, + audioFileId: number, + ): Promise { + try { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTask.name + } | params: { externalId: ${externalId}, audioFileId: ${audioFileId} };`, + ); + + // 実行ユーザーの情報を取得する + const user = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + const account = user.account; + if (!account) { + throw new Error(`account not found. externalId: ${externalId}`); + } + + // 削除対象の音声ファイル情報を取得する + const task = await this.taskRepository.getTaskAndAudioFile( + context, + audioFileId, + user.account_id, + Object.values(TASK_STATUS), + ); + + const targetFileName = task.file?.raw_file_name; + if (!targetFileName) { + throw new Error(`target file not found. audioFileId: ${audioFileId}`); + } + + // DBからタスクと紐づくデータを削除する + await this.taskRepository.deleteTask(context, user.id, audioFileId); + + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行するため、try-catchで囲む + try { + // BlobStorageから音声ファイルを削除する + await this.blobStorageService.deleteFile( + context, + account.id, + account.country, + targetFileName, + ); + } catch (e) { + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${ + account.id + }, fileName: ${targetFileName}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case StatusNotMatchError: + throw new HttpException( + makeErrorResponse('E010601'), + HttpStatus.BAD_REQUEST, + ); + case TaskAuthorIdNotMatchError: + throw new HttpException( + makeErrorResponse('E010602'), + HttpStatus.BAD_REQUEST, + ); + case TasksNotFoundError: + throw new HttpException( + makeErrorResponse('E010603'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTask.name}`, + ); + } + } + // 通知を送信するプライベートメソッド private async sendNotify( context: Context, diff --git a/dictation_server/src/features/tasks/test/tasks.service.mock.ts b/dictation_server/src/features/tasks/test/tasks.service.mock.ts index 024f763..f33c963 100644 --- a/dictation_server/src/features/tasks/test/tasks.service.mock.ts +++ b/dictation_server/src/features/tasks/test/tasks.service.mock.ts @@ -17,6 +17,7 @@ import { NotificationhubService } from '../../../gateways/notificationhub/notifi import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service'; +import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; import { LicensesRepositoryMockValue, makeLicensesRepositoryMock, @@ -98,6 +99,8 @@ export const makeTasksServiceMock = async ( // メール送信でしか利用しておらず、テストする必要がないが、依存関係解決のため空オブジェクトを定義しておく。 case SendGridService: return {}; + case BlobstorageService: + return {}; case LicensesRepositoryService: return makeLicensesRepositoryMock(licensesRepositoryMockValue); } @@ -458,6 +461,7 @@ const defaultTasksRepositoryMockValue: { owner_user_id: 1, url: 'test/test.zip', file_name: 'test.zip', + raw_file_name: 'test.zip', author_id: 'AUTHOR', work_type_id: 'WorkType', started_at: new Date('2023-01-01T01:01:01.000Z'), diff --git a/dictation_server/src/features/tasks/test/utility.ts b/dictation_server/src/features/tasks/test/utility.ts index 0f21df0..b0c9637 100644 --- a/dictation_server/src/features/tasks/test/utility.ts +++ b/dictation_server/src/features/tasks/test/utility.ts @@ -120,6 +120,7 @@ export const createTask = async ( owner_user_id: owner_user_id, url: '', file_name: 'x.zip', + raw_file_name: 'y.zip', author_id: author_id, work_type_id: work_type_id, started_at: new Date(), @@ -157,6 +158,7 @@ export const createTask = async ( created_at: new Date(), }); const task = taskIdentifiers.pop() as Task; + return { taskId: task.id, audioFileId: audioFile.id }; }; @@ -272,3 +274,23 @@ export const getCheckoutPermissions = async ( }); return permissions; }; + +export const getAudioFile = async ( + datasource: DataSource, + audio_file_id: number, +): Promise => { + const audioFile = await datasource.getRepository(AudioFile).findOne({ + where: { id: audio_file_id }, + }); + return audioFile; +}; + +export const getAudioOptionItems = async ( + datasource: DataSource, + audio_file_id: number, +): Promise => { + const audioOptionItems = await datasource + .getRepository(AudioOptionItem) + .find({ where: { audio_file_id: audio_file_id } }); + return audioOptionItems; +}; diff --git a/dictation_server/src/features/tasks/types/convert.ts b/dictation_server/src/features/tasks/types/convert.ts index 2ce6d99..c7fb43e 100644 --- a/dictation_server/src/features/tasks/types/convert.ts +++ b/dictation_server/src/features/tasks/types/convert.ts @@ -66,6 +66,7 @@ const createTask = ( audioFormat: file.audio_format, comment: file.comment ?? '', fileName: file.file_name, + rawFileName: file.raw_file_name, fileSize: file.file_size, isEncrypted: file.is_encrypted, url: file.url, diff --git a/dictation_server/src/features/tasks/types/types.ts b/dictation_server/src/features/tasks/types/types.ts index 2a6ed96..34d4252 100644 --- a/dictation_server/src/features/tasks/types/types.ts +++ b/dictation_server/src/features/tasks/types/types.ts @@ -110,6 +110,8 @@ export class Task { url: string; @ApiProperty({ description: '音声ファイル名' }) fileName: string; + @ApiProperty({ description: '生(Blob Storage上の)音声ファイル名' }) + rawFileName: string; @ApiProperty({ description: '音声ファイルの録音時間(ミリ秒の整数値)', }) @@ -227,3 +229,13 @@ export class PostCheckoutPermissionRequest { } export class PostCheckoutPermissionResponse {} + +export class PostDeleteTaskRequest { + @ApiProperty({ description: 'ODMS Cloud上の音声ファイルID' }) + @Type(() => Number) + @IsInt() + @Min(1) + audioFileId: number; +} + +export class PostDeleteTaskResponse {} diff --git a/dictation_server/src/features/templates/templates.controller.ts b/dictation_server/src/features/templates/templates.controller.ts index d07db64..e98bcdf 100644 --- a/dictation_server/src/features/templates/templates.controller.ts +++ b/dictation_server/src/features/templates/templates.controller.ts @@ -4,6 +4,8 @@ import { HttpException, HttpStatus, Logger, + Param, + Post, Req, UseGuards, } from '@nestjs/common'; @@ -16,7 +18,11 @@ import { import jwt from 'jsonwebtoken'; import { AccessToken } from '../../common/token'; import { ErrorResponse } from '../../common/error/types/types'; -import { GetTemplatesResponse } from './types/types'; +import { + DeleteTemplateRequestParam, + DeleteTemplateResponse, + GetTemplatesResponse, +} from './types/types'; import { AuthGuard } from '../../common/guards/auth/authguards'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { ADMIN_ROLES } from '../../constants'; @@ -97,4 +103,83 @@ export class TemplatesController { return { templates }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: DeleteTemplateResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: + 'ルーティングルールに設定されている / 未完了タスクに紐づいている / 削除済み', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteTemplateFile', + description: + 'ログインしているユーザーのアカウント配下でIDで指定されたテンプレートファイルを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post(':templateFileId/delete') + async deleteTemplateFile( + @Req() req: Request, + @Param() param: DeleteTemplateRequestParam, + ): Promise { + const { templateFileId } = param; + + // アクセストークン取得 + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + await this.templatesService.deleteTemplate(context, userId, templateFileId); + + return {}; + } } diff --git a/dictation_server/src/features/templates/templates.module.ts b/dictation_server/src/features/templates/templates.module.ts index ac14580..fb93de7 100644 --- a/dictation_server/src/features/templates/templates.module.ts +++ b/dictation_server/src/features/templates/templates.module.ts @@ -3,9 +3,14 @@ import { TemplatesController } from './templates.controller'; import { TemplatesService } from './templates.service'; import { UsersRepositoryModule } from '../../repositories/users/users.repository.module'; import { TemplateFilesRepositoryModule } from '../../repositories/template_files/template_files.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ - imports: [UsersRepositoryModule, TemplateFilesRepositoryModule], + imports: [ + UsersRepositoryModule, + TemplateFilesRepositoryModule, + BlobstorageModule, + ], providers: [TemplatesService], controllers: [TemplatesController], }) diff --git a/dictation_server/src/features/templates/templates.service.spec.ts b/dictation_server/src/features/templates/templates.service.spec.ts index 8ef101f..aca4887 100644 --- a/dictation_server/src/features/templates/templates.service.spec.ts +++ b/dictation_server/src/features/templates/templates.service.spec.ts @@ -1,13 +1,31 @@ import { DataSource } from 'typeorm'; import { makeTestingModule } from '../../common/test/modules'; import { TemplatesService } from './templates.service'; -import { createTemplateFile } from './test/utility'; -import { makeTestAccount } from '../../common/test/utility'; +import { + createTemplateFile, + getTemplateFiles, + updateTaskTemplateFile, +} from './test/utility'; +import { + getTasks, + makeTestAccount, + makeTestUser, +} from '../../common/test/utility'; import { makeContext } from '../../common/log'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { overrideBlobstorageService } from '../../common/test/overrides'; +import { TASK_STATUS, USER_ROLES } from '../../constants'; +import { createTask } from '../tasks/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflow, +} from '../workflows/test/utility'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { TestLogger } from '../../common/test/logger'; describe('getTemplates', () => { let source: DataSource | null = null; @@ -23,6 +41,8 @@ describe('getTemplates', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -129,3 +149,357 @@ describe('getTemplates', () => { } }); }); + +describe('deleteTemplate', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('指定したテンプレートファイルを削除できる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + const blobStorageService = + module.get(BlobstorageService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + const template2 = await createTemplateFile( + source, + account.id, + 'test2', + 'https://url2/test2', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.FINISHED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + const { taskId: taskId2 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000002', + TASK_STATUS.BACKUP, + ); + await updateTaskTemplateFile(source, taskId2, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(2); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + expect(templates[1].id).toBe(template2.id); + expect(templates[1].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[1].template_file_id).toBe(template1.id); + } + + await service.deleteTemplate(context, admin.external_id, template1.id); + + //実行結果を確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template2.id); + expect(templates[0].file_name).toBe(template2.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(2); + expect(tasks[0].template_file_id).toBe(null); + expect(tasks[1].template_file_id).toBe(null); + + // Blob削除メソッドが呼ばれているか確認 + expect(blobStorageService.deleteFile).toBeCalledWith( + context, + account.id, + account.country, + 'Templates/test1', + ); + } + }); + + it('指定したテンプレートファイルが存在しない場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, 9999); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016001')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルがルーティングルールに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const { id: typistId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.TYPIST, + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { id: workflowId } = await createWorkflow( + source, + account.id, + authorId, + undefined, + template1.id, + ); + await createWorkflowTypist(source, workflowId, typistId); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const workflow = await getWorkflow(source, account.id, workflowId); + expect(workflow?.template_id).toBe(template1.id); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016002')); + } else { + fail(); + } + } + }); + + it('指定したテンプレートファイルが未完了のタスクに紐づく場合、400エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const { id: authorId } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'authorId', + }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + const { taskId: taskId1 } = await createTask( + source, + account.id, + authorId, + 'authorId', + '', + '01', + '00000001', + TASK_STATUS.UPLOADED, + ); + await updateTaskTemplateFile(source, taskId1, template1.id); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + + const tasks = await getTasks(source, account.id); + expect(tasks.length).toBe(1); + expect(tasks[0].template_file_id).toBe(template1.id); + expect(tasks[0].status).toBe(TASK_STATUS.UPLOADED); + } + + //実行結果を確認 + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E016003')); + } else { + fail(); + } + } + }); + + it('DBアクセスに失敗した場合、500エラーとなる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const service = module.get(TemplatesService); + overrideBlobstorageService(service, { + deleteFile: jest.fn(), + }); + + // 第五階層のアカウント作成 + const { account, admin } = await makeTestAccount(source, { tier: 5 }); + const context = makeContext(admin.external_id, 'requestId'); + + const template1 = await createTemplateFile( + source, + account.id, + 'test1', + 'https://url1/test1', + ); + + // 作成したデータを確認 + { + const templates = await getTemplateFiles(source, account.id); + expect(templates.length).toBe(1); + expect(templates[0].id).toBe(template1.id); + expect(templates[0].file_name).toBe(template1.file_name); + } + + //DBアクセスに失敗するようにする + const templateFilesRepositoryService = + module.get( + TemplateFilesRepositoryService, + ); + templateFilesRepositoryService.getTemplateFiles = jest + .fn() + .mockRejectedValue('DB failed'); + + try { + await service.deleteTemplate(context, admin.external_id, template1.id); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); diff --git a/dictation_server/src/features/templates/templates.service.ts b/dictation_server/src/features/templates/templates.service.ts index ae36afe..36b2549 100644 --- a/dictation_server/src/features/templates/templates.service.ts +++ b/dictation_server/src/features/templates/templates.service.ts @@ -4,6 +4,13 @@ import { TemplateFile } from './types/types'; import { Context } from '../../common/log'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { TemplateFilesRepositoryService } from '../../repositories/template_files/template_files.repository.service'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from '../../repositories/template_files/errors/types'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; +import { MANUAL_RECOVERY_REQUIRED } from '../../constants'; @Injectable() export class TemplatesService { @@ -11,6 +18,7 @@ export class TemplatesService { constructor( private readonly usersRepository: UsersRepositoryService, private readonly templateFilesRepository: TemplateFilesRepositoryService, + private readonly blobStorageService: BlobstorageService, ) {} /** @@ -55,4 +63,103 @@ export class TemplatesService { ); } } + + /** + * アカウント内の指定されたテンプレートファイルを削除する + * @param context + * @param externalId + * @param templateFileId + * @returns template + */ + async deleteTemplate( + context: Context, + externalId: string, + templateFileId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteTemplate.name + } | params: { externalId: ${externalId}, templateFileId: ${templateFileId} };`, + ); + + try { + const { account } = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + if (!account) { + throw new Error(`account not found. externalId: ${externalId}`); + } + + // テンプレートファイルの取得 + const templateFile = await this.templateFilesRepository.getTemplateFile( + context, + account.id, + templateFileId, + ); + + // DBからのテンプレートファイルの削除 + await this.templateFilesRepository.deleteTemplateFile( + context, + account.id, + templateFileId, + ); + + try { + // Blob Storageからのテンプレートファイルの削除 + await this.blobStorageService.deleteFile( + context, + account.id, + account.country, + `Templates/${templateFile.file_name}`, + ); + } catch (e) { + // Blob削除失敗時は、MANUAL_RECOVERY_REQUIREDを出して処理続行 + this.logger.log(`[${context.getTrackingId()}] ${e}`); + this.logger.log( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete Blob: accountId: ${ + account.id + }, fileName: ${templateFile.file_name}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + // 指定されたIDのテンプレートファイルが存在しない + case TemplateFileNotExistError: + throw new HttpException( + makeErrorResponse('E016001'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルがルーティングルールに設定されている + case WorkflowHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016002'), + HttpStatus.BAD_REQUEST, + ); + // 指定されたIDのテンプレートファイルが未完了タスクに紐づいている + case NotFinishedTaskHasTemplateDeleteFailedError: + throw new HttpException( + makeErrorResponse('E016003'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteTemplate.name}`, + ); + } + } } diff --git a/dictation_server/src/features/templates/test/utility.ts b/dictation_server/src/features/templates/test/utility.ts index 224b536..1358df1 100644 --- a/dictation_server/src/features/templates/test/utility.ts +++ b/dictation_server/src/features/templates/test/utility.ts @@ -1,5 +1,6 @@ import { DataSource } from 'typeorm'; import { TemplateFile } from '../../../repositories/template_files/entity/template_file.entity'; +import { Task } from '../../../repositories/tasks/entity/task.entity'; export const createTemplateFile = async ( datasource: DataSource, @@ -41,3 +42,18 @@ export const getTemplateFiles = async ( }); return templates; }; + +export const updateTaskTemplateFile = async ( + datasource: DataSource, + taskId: number, + templateFileId: number, +): Promise => { + await datasource.getRepository(Task).update( + { id: taskId }, + { + template_file_id: templateFileId, + updated_by: 'updater', + updated_at: new Date(), + }, + ); +}; diff --git a/dictation_server/src/features/templates/types/types.ts b/dictation_server/src/features/templates/types/types.ts index 243298d..f1959d3 100644 --- a/dictation_server/src/features/templates/types/types.ts +++ b/dictation_server/src/features/templates/types/types.ts @@ -1,4 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, Min } from 'class-validator'; export class TemplateFile { @ApiProperty({ description: 'テンプレートファイルのID' }) @@ -14,3 +16,13 @@ export class GetTemplatesResponse { }) templates: TemplateFile[]; } + +export class DeleteTemplateRequestParam { + @ApiProperty() + @Type(() => Number) + @IsInt() + @Min(1) + templateFileId: number; +} + +export class DeleteTemplateResponse {} diff --git a/dictation_server/src/features/terms/terms.service.spec.ts b/dictation_server/src/features/terms/terms.service.spec.ts index 3607f41..79552ba 100644 --- a/dictation_server/src/features/terms/terms.service.spec.ts +++ b/dictation_server/src/features/terms/terms.service.spec.ts @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('利用規約取得', () => { let source: DataSource | null = null; @@ -22,6 +23,8 @@ describe('利用規約取得', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index c2da3db..f17d58e 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -19,6 +19,7 @@ import { import { AdB2cUser } from '../../../gateways/adb2c/types/types'; import { ADB2C_SIGN_IN_TYPE } from '../../../constants'; import { AccountsRepositoryService } from '../../../repositories/accounts/accounts.repository.service'; +import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service'; export type SortCriteriaRepositoryMockValue = { updateSortCriteria: SortCriteria | Error; @@ -89,6 +90,8 @@ export const makeUsersServiceMock = async ( return makeSortCriteriaRepositoryMock( sortCriteriaRepositoryMockValue, ); + case BlobstorageService: + return {}; } }) .compile(); diff --git a/dictation_server/src/features/users/test/utility.ts b/dictation_server/src/features/users/test/utility.ts index 849aa34..99a6413 100644 --- a/dictation_server/src/features/users/test/utility.ts +++ b/dictation_server/src/features/users/test/utility.ts @@ -165,16 +165,3 @@ export const makeTestingModuleWithAdb2c = async ( console.log(e); } }; - -export const createSortCriteria = async ( - datasource: DataSource, - userId: number, - parameter: string, - direction: string, -): Promise => { - await datasource.getRepository(SortCriteria).insert({ - user_id: userId, - parameter: parameter, - direction: direction, - }); -}; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index 6f4edf9..2b5e8a4 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -1,11 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; import { + IsArray, IsBoolean, IsEmail, IsIn, IsInt, + IsNotEmpty, IsOptional, + IsString, MaxLength, + ValidateIf, + ValidateNested, } from 'class-validator'; import { TASK_LIST_SORTABLE_ATTRIBUTES, @@ -18,7 +23,10 @@ import { } from '../../../common/validators/encryptionPassword.validator'; import { IsRoleAuthorDataValid } from '../../../common/validators/roleAuthor.validator'; import { Type } from 'class-transformer'; -import { IsAuthorIdValid } from '../../../common/validators/authorId.validator'; +import { + IsAuthorID, + IsAuthorIdValid, +} from '../../../common/validators/authorId.validator'; export class ConfirmRequest { @ApiProperty() @@ -255,6 +263,130 @@ export class PostUpdateUserRequest { export class PostUpdateUserResponse {} +export class PostDeleteUserRequest { + @ApiProperty({ description: '削除対象のユーザーID' }) + @Type(() => Number) + @IsInt() + userId: number; +} + +export class PostDeleteUserResponse {} + +export class MultipleImportUser { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @MaxLength(256) // AzureAdB2Cの仕様上、256文字まで[https://learn.microsoft.com/ja-jp/azure/active-directory-b2c/user-profile-attributes] + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'メールアドレス' }) + @IsEmail({ blacklisted_chars: '*' }) + @IsNotEmpty() + email: string; + + @ApiProperty({ description: '0(none)/1(author)/2(typist)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1, 2]) + role: number; + + @ApiProperty({ required: false }) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + @IsAuthorID() + @IsNotEmpty() + authorId?: string; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + autoRenew: number; + + @ApiProperty({ description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + notification: number; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + encryption?: number; + + @ApiProperty({ required: false }) + @IsPasswordvalid() + @IsNotEmpty() + @IsString() + @ValidateIf((o) => o.role === 1 && o.encryption === 1) // roleがauthorかつencryptionがtrueの場合のみバリデーションを実施 + encryptionPassword?: string; + + @ApiProperty({ required: false, description: '0(false)/1(true)' }) + @Type(() => Number) + @IsInt() + @IsIn([0, 1]) + @ValidateIf((o) => o.role === 1) // roleがauthorの場合のみバリデーションを実施 + prompt?: number; +} + +export class PostMultipleImportsRequest { + @ApiProperty({ description: 'CSVファイル名' }) + @IsString() + @IsNotEmpty() + filename: string; + + @ApiProperty({ type: [MultipleImportUser] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportUser) + users: MultipleImportUser[]; +} +export class PostMultipleImportsResponse {} + +export class MultipleImportErrors { + @ApiProperty({ description: 'ユーザー名' }) + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty({ description: 'エラー発生行数' }) + @IsInt() + @IsNotEmpty() + @Type(() => Number) + line: number; + + @ApiProperty({ description: 'エラーコード' }) + @IsString() + @IsNotEmpty() + errorCode: string; +} + +export class PostMultipleImportsCompleteRequest { + @ApiProperty({ description: 'アカウントID' }) + @Type(() => Number) + @IsInt() + accountId: number; + + @ApiProperty({ description: 'CSVファイル名' }) + @IsString() + @IsNotEmpty() + filename: string; + + @ApiProperty({ description: '一括登録受付時刻(UNIXTIME/ミリ秒)' }) + @IsInt() + @IsNotEmpty() + @Type(() => Number) + requestTime: number; + + @ApiProperty({ type: [MultipleImportErrors] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => MultipleImportErrors) + errors: MultipleImportErrors[]; +} +export class PostMultipleImportsCompleteResponse {} + export class AllocateLicenseRequest { @ApiProperty({ description: 'ユーザーID' }) @Type(() => Number) diff --git a/dictation_server/src/features/users/users.controller.spec.ts b/dictation_server/src/features/users/users.controller.spec.ts index d060ac2..123c403 100644 --- a/dictation_server/src/features/users/users.controller.spec.ts +++ b/dictation_server/src/features/users/users.controller.spec.ts @@ -3,6 +3,12 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { ConfigModule } from '@nestjs/config'; import { AuthService } from '../auth/auth.service'; +import { + PostMultipleImportsCompleteRequest, + PostMultipleImportsRequest, +} from './types/types'; +import { validate } from 'class-validator'; +import { plainToClass } from 'class-transformer'; describe('UsersController', () => { let controller: UsersController; @@ -32,4 +38,538 @@ describe('UsersController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('valdation PostMultipleImportsRequest', () => { + it('role:noneの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('role:authorの最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('emailがメールアドレスではない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge', + role: 0, + autoRenew: 0, + notification: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('AuthorなのにAuthorIDがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('AuthorIDがルールに違反していた場合、バリデーションエラーが発生する', async () => { + // ルールに合致したAuthorIDではエラーが発生しない + const validAuthorIDs = ['A', '_', 'AB', 'A1', '1A', '_1', 'A_B']; + for await (const authorId of validAuthorIDs) { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: authorId, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + } + + // ルールに違反したAuthorIDではエラーが発生する + const invalidAuthorIDs = ['a', '+', 'AB.', 'Ab', '1a', '_.', 'A/B', '']; + for await (const authorId of invalidAuthorIDs) { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: authorId, + autoRenew: 0, + notification: 0, + encryption: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + } + }); + it('Authorなのにencryptionがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('Authorなのにpromptがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('Authorでencryption:trueなのに、encryptionPasswordがない場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('Authorでencryption:trueでencryptionPasswordが正常であれば成功する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcd', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('encryptionPasswordが要件外(短い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('encryptionPasswordが要件外(長い)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('encryptionPasswordが要件外(全角が含まれる)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'AUTHOR', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abcあいうえお', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('AuthorIDが要件外(小文字)の場合、バリデーションエラーが発生する', async () => { + const request = new PostMultipleImportsRequest(); + request.filename = 'test.csv'; + request.users = [ + { + name: 'namae', + email: 'hogehoge@example.com', + role: 1, + authorId: 'author', + autoRenew: 0, + notification: 0, + encryption: 1, + encryptionPassword: 'abc', + prompt: 0, + }, + ]; + + const valdationObject = plainToClass(PostMultipleImportsRequest, request); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); + + describe('valdation PostMultipleImportsCompleteRequest', () => { + it('最低限の有効なリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.requestTime = new Date().getTime(); + request.filename = 'test.csv'; + request.errors = []; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('ファイル名が存在しなかった場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + requestTime: new Date().getTime(), + errors: [], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('エラーが存在するリクエストが成功する', async () => { + const request = new PostMultipleImportsCompleteRequest(); + request.accountId = 1; + request.requestTime = new Date().getTime(); + request.filename = 'test.csv'; + request.errors = [ + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ]; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(0); + }); + + it('名前が足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + line: 1, + errorCode: 'E1101', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('行指定が足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + name: 'namae', + errorCode: 'E1101', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('errorCodeが足りないエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + name: 'namae', + line: 1, + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + + it('名前が空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + name: '', + line: 1, + errorCode: 'E1101', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('行数が空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + name: 'namae', + errorCode: 'E1101', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + it('エラーコードが空のエラーがある場合、バリデーションエラーが発生する', async () => { + const request = { + accountId: 1, + filename: 'test.csv', + requestTime: new Date().getTime(), + errors: [ + { + name: 'namae', + line: 1, + errorCode: '', + }, + { + name: 'namae', + line: 1, + errorCode: 'E1101', + }, + ], + }; + + const valdationObject = plainToClass( + PostMultipleImportsCompleteRequest, + request, + ); + + const errors = await validate(valdationObject); + expect(errors.length).toBe(1); + }); + }); }); diff --git a/dictation_server/src/features/users/users.controller.ts b/dictation_server/src/features/users/users.controller.ts index d607e50..7d731cc 100644 --- a/dictation_server/src/features/users/users.controller.ts +++ b/dictation_server/src/features/users/users.controller.ts @@ -4,7 +4,6 @@ import { Get, HttpException, HttpStatus, - Ip, Logger, Post, Query, @@ -42,6 +41,12 @@ import { UpdateAcceptedVersionRequest, UpdateAcceptedVersionResponse, GetMyUserResponse, + PostDeleteUserRequest, + PostDeleteUserResponse, + PostMultipleImportsRequest, + PostMultipleImportsResponse, + PostMultipleImportsCompleteRequest, + PostMultipleImportsCompleteResponse, } from './types/types'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; @@ -55,6 +60,8 @@ import { ADMIN_ROLES, TIERS } from '../../constants'; import { RoleGuard } from '../../common/guards/role/roleguards'; import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log'; import { UserRoles } from '../../common/types/role'; +import { SystemAccessGuard } from '../../common/guards/system/accessguards'; +import { SystemAccessToken } from '../../common/token/types'; @ApiTags('users') @Controller('users') @@ -916,4 +923,232 @@ export class UsersController { const userName = await this.usersService.getUserName(context, userId); return { userName }; } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostDeleteUserResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'deleteUser', + description: 'ユーザーを削除します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('delete') + async deleteUser( + @Body() body: PostDeleteUserRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedAccessToken = jwt.decode(accessToken, { json: true }); + if (!decodedAccessToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId } = decodedAccessToken as AccessToken; + + const context = makeContext(userId, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + const now = new Date(); + await this.usersService.deleteUser(context, body.userId, now); + return {}; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImports', + description: 'ユーザーを一括登録します', + }) + @ApiBearerAuth() + @UseGuards(AuthGuard) + @UseGuards( + RoleGuard.requireds({ roles: [ADMIN_ROLES.ADMIN], delegation: true }), + ) + @Post('multiple-imports') + async multipleImports( + @Body() body: PostMultipleImportsRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { userId, delegateUserId } = decodedToken as AccessToken; + const context = makeContext(userId, requestId, delegateUserId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + // 登録処理 + const { users, filename } = body; + await this.usersService.multipleImports(context, userId, filename, users); + + return {}; + } + + @ApiResponse({ + status: HttpStatus.OK, + type: PostMultipleImportsCompleteResponse, + description: '成功時のレスポンス', + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: '不正なパラメータ', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.UNAUTHORIZED, + description: '認証エラー', + type: ErrorResponse, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: '想定外のサーバーエラー', + type: ErrorResponse, + }) + @ApiOperation({ + operationId: 'multipleImportsComplate', + description: 'ユーザー一括登録の完了を通知します', + }) + @ApiBearerAuth() + @UseGuards(SystemAccessGuard) + @Post('multiple-imports/complete') + async multipleImportsComplate( + @Body() body: PostMultipleImportsCompleteRequest, + @Req() req: Request, + ): Promise { + const accessToken = retrieveAuthorizationToken(req); + if (!accessToken) { + throw new HttpException( + makeErrorResponse('E000107'), + HttpStatus.UNAUTHORIZED, + ); + } + + const ip = retrieveIp(req); + if (!ip) { + throw new HttpException( + makeErrorResponse('E000401'), + HttpStatus.UNAUTHORIZED, + ); + } + + const requestId = retrieveRequestId(req); + if (!requestId) { + throw new HttpException( + makeErrorResponse('E000501'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + const decodedToken = jwt.decode(accessToken, { json: true }); + if (!decodedToken) { + throw new HttpException( + makeErrorResponse('E000101'), + HttpStatus.UNAUTHORIZED, + ); + } + const { systemName } = decodedToken as SystemAccessToken; + const context = makeContext(systemName, requestId); + this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`); + + const { accountId, filename, requestTime, errors } = body; + await this.usersService.multipleImportsComplate( + context, + accountId, + filename, + requestTime, + errors, + ); + + return {}; + } } diff --git a/dictation_server/src/features/users/users.module.ts b/dictation_server/src/features/users/users.module.ts index b5f5963..f8d02d9 100644 --- a/dictation_server/src/features/users/users.module.ts +++ b/dictation_server/src/features/users/users.module.ts @@ -9,6 +9,7 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { AuthService } from '../auth/auth.service'; import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.repository.module'; +import { BlobstorageModule } from '../../gateways/blobstorage/blobstorage.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { AccountsRepositoryModule } from '../../repositories/accounts/accounts.r AdB2cModule, SendGridModule, ConfigModule, + BlobstorageModule, ], controllers: [UsersController], providers: [UsersService, AuthService], diff --git a/dictation_server/src/features/users/users.service.spec.ts b/dictation_server/src/features/users/users.service.spec.ts index bb4931c..899167e 100644 --- a/dictation_server/src/features/users/users.service.spec.ts +++ b/dictation_server/src/features/users/users.service.spec.ts @@ -8,7 +8,6 @@ import { makeDefaultUsersRepositoryMockValue, makeUsersServiceMock, } from './test/users.service.mock'; -import { EmailAlreadyVerifiedError } from '../../repositories/users/errors/types'; import { createLicense, createUserGroup, @@ -18,9 +17,12 @@ import { import { DataSource } from 'typeorm'; import { UsersService } from './users.service'; import { + ADB2C_SIGN_IN_TYPE, LICENSE_ALLOCATED_STATUS, LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_TYPE, + MANUAL_RECOVERY_REQUIRED, + TASK_STATUS, USER_AUDIO_FORMAT, USER_LICENSE_EXPIRY_STATUS, USER_ROLES, @@ -29,6 +31,7 @@ import { makeTestingModule } from '../../common/test/modules'; import { Context, makeContext } from '../../common/log'; import { overrideAdB2cService, + overrideBlobstorageService, overrideSendgridService, overrideUsersRepositoryService, } from '../../common/test/overrides'; @@ -37,6 +40,7 @@ import { License } from '../../repositories/licenses/entity/license.entity'; import { AdB2cService } from '../../gateways/adb2c/adb2c.service'; import { getUser, + getUserArchive, getUserFromExternalId, getUsers, makeTestAccount, @@ -45,8 +49,17 @@ import { } from '../../common/test/utility'; import { v4 as uuidv4 } from 'uuid'; import { createOptionItems, createWorktype } from '../accounts/test/utility'; -import { createWorkflow, getWorkflows } from '../workflows/test/utility'; +import { + createWorkflow, + createWorkflowTypist, + getWorkflows, +} from '../workflows/test/utility'; import { truncateAllTable } from '../../common/test/init'; +import { createTask } from '../files/test/utility'; +import { createCheckoutPermissions } from '../tasks/test/utility'; +import { MultipleImportErrors } from './types/types'; +import { TestLogger } from '../../common/test/logger'; +import { SendGridService } from '../../gateways/sendgrid/sendgrid.service'; describe('UsersService.confirmUser', () => { let source: DataSource | null = null; @@ -62,6 +75,8 @@ describe('UsersService.confirmUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -240,216 +255,564 @@ describe('UsersService.confirmUser', () => { }); describe('UsersService.confirmUserAndInitPassword', () => { + let source: DataSource | null = null; + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); it('ユーザーが発行されたパスワードでログインできるようにする', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + let _subject: string = ''; + overrideSendgridService(service, { + sendMail: async ( + context: Context, + to: string[], + cc: string[], + from: string, + subject: string, + text: string, + html: string, + ) => { + _subject = subject; + }, + }); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - expect( + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + expect(_subject).toBe('Temporary password [U-113]'); + const user = await getUserFromExternalId(source, 'externalId_user1'); + expect(user?.email_verified).toBe(true); + }); + it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = 'invalid.id.token'; + + try { await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).toEqual(undefined); - }); - - it('トークンの形式が不正な場合、形式不正エラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, - ); - const token = 'invalid.id.token'; - await expect( - service.confirmUserAndInitPassword( - makeContext('trackingId', 'requestId'), - token, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E000101'), HttpStatus.BAD_REQUEST), - ); + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E000101')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); }); it('ユーザが既に認証済みだった場合、認証済みユーザエラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: true, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, auto_renew: true, - notification: true, encryption: false, + encryption_password: undefined, prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - usersRepositoryMockValue.updateUserVerified = new EmailAlreadyVerifiedError( - `Email already verified user`, - ); + email_verified: true, // emailを認証済みにする + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, - ); const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - await expect( - service.confirmUserAndInitPassword( + + try { + await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).rejects.toEqual( - new HttpException(makeErrorResponse('E010202'), HttpStatus.BAD_REQUEST), + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E010202')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていることを確認 + expect(user?.email_verified).toBe(true); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); + }); + it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行い、メールを未認証のままにする。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: async () => { + throw new Error('ADB2C Error'); + }, + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + }); + it('ADB2Cユーザーのパスワード更新に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗すると認証のままになる(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + overrideAdB2cService(service, { + changePassword: async () => { + throw new Error('ADB2C Error'); + }, + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideUsersRepositoryService(service, { + updateUserUnverified: async () => { + throw new Error('DB Error'); + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されたままであることを確認 + expect(user?.email_verified).toBe(true); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true); }); it('DBネットワークエラーとなる場合、エラーとなる。(メール認証API)', async () => { - const usersRepositoryMockValue = makeDefaultUsersRepositoryMockValue(); - usersRepositoryMockValue.findUserById = { - id: 1, - external_id: 'TEST9999', - account_id: 1, - role: 'None', - accepted_eula_version: 'string', - accepted_privacy_notice_version: 'string', - accepted_dpa_version: 'string', - email_verified: false, - created_by: 'string;', - created_at: new Date(), - updated_by: 'string;', - updated_at: new Date(), - auto_renew: true, - notification: true, - encryption: false, - prompt: false, - account: null, - author_id: null, - deleted_at: null, - encryption_password: null, - license: null, - userGroupMembers: null, - }; - const licensesRepositoryMockValue = null; - const adb2cParam = makeDefaultAdB2cMockValue(); - const sendGridMockValue = makeDefaultSendGridlValue(); - usersRepositoryMockValue.updateUserVerified = new Error('DB error'); - const configMockValue = makeDefaultConfigValue(); - const sortCriteriaRepositoryMockValue = - makeDefaultSortCriteriaRepositoryMockValue(); - const service = await makeUsersServiceMock( - usersRepositoryMockValue, - licensesRepositoryMockValue, - adb2cParam, - sendGridMockValue, - configMockValue, - sortCriteriaRepositoryMockValue, + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const sendgridService = module.get(SendGridService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: jest.fn(), + }); + // DBエラーを発生させる + overrideUsersRepositoryService(service, { + updateUserVerified: async () => { + throw new Error('DB Error'); + }, + }); + const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; - await expect( - service.confirmUserAndInitPassword( + try { + await service.confirmUserAndInitPassword( makeContext('trackingId', 'requestId'), token, - ), - ).rejects.toEqual( - new HttpException( - makeErrorResponse('E009999'), - HttpStatus.INTERNAL_SERVER_ERROR, - ), + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // メールが送信されていないことを確認 + expect(sendgridService.sendMail).toBeCalledTimes(0); + // パスワードが変更されていないことを確認 + expect(adB2cService.changePassword).toBeCalledTimes(0); + }); + it('メール送信に失敗した場合、リカバリ処理を行い、メールを未認証の状態にする。(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adb2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('SendGrid Error'); + }, + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されていないことを確認 + expect(user?.email_verified).toBe(false); + // ADB2Cのパスワードが変更されていることを確認(パスワードは変更されても、ユーザーにメールが届いていないので問題ない) + expect(adb2cService.changePassword).toBeCalledTimes(1); + }); + it('メール送信に失敗した場合、リカバリ処理を行うが、リカバリ処理に失敗するとADB2Cのパスワードが変更され、DB上も認証された状態になる(メール認証API)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + const service = module.get(UsersService); + const adB2cService = module.get(AdB2cService); + const adminExternalId = 'ADMIN0001'; + const { account } = await makeTestAccount( + source, + {}, + { external_id: adminExternalId }, + ); + const { id: accountId } = account; + // ユーザー作成 + await makeTestUser(source, { + account_id: accountId, + external_id: 'externalId_user1', + role: USER_ROLES.NONE, + author_id: undefined, + auto_renew: true, + encryption: false, + encryption_password: undefined, + prompt: false, + email_verified: false, + }); + const loggerSpy = jest + .spyOn(service['logger'], 'error') + .mockImplementation(); + + overrideAdB2cService(service, { + changePassword: jest.fn(), + getUser: async () => { + return { + id: adminExternalId, + displayName: 'admin', + }; + }, + }); + overrideUsersRepositoryService(service, { + updateUserUnverified: async () => { + throw new Error('DB Error'); + }, + }); + overrideSendgridService(service, { + sendMail: async () => { + throw new Error('SendGrid Error'); + }, + }); + + const token = + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50SWQiOjEsInVzZXJJZCI6MiwiZW1haWwiOiJ4eHhAeHh4Lnh4eCIsImlhdCI6MTAwMDAwMDAwMCwiZXhwIjo5MDAwMDAwMDAwfQ.26L6BdNg-3TbyKT62PswlJ6RPMkcTtHzlDXW2Uo9XbMPVSrl2ObcuS6EcXjFFN2DEfNTKbqX_zevIWMpHOAdLNgGhk528nLrBrNvPASqtTjvW9muxMXpjUdjRVkmVbOylBHWW3YpWL9JEbJQ7rAzWDfaIdPhMovdaxumnZt_UwnlnrdaVPLACW7tkH_laEcAU507iSiM4mqxxG8FuTs34t6PEdwRuzZAQPN2IOPYNSvGNdJYryPacSeSNZ_z1xeBYXLOLQfOBZzyTReYDOhXdikhrNUbxjgnZQlSXBCVMlZ9PH42bHfp-LJIeJzW0yqnF6oLklvJP-fo8eW0k5iDOw'; + try { + await service.confirmUserAndInitPassword( + makeContext('trackingId', 'requestId'), + token, + ); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + const user = await getUserFromExternalId(source, 'externalId_user1'); + // ユーザーが認証されたままであることを確認 + expect(user?.email_verified).toBe(true); + // ADB2Cのパスワードが変更されていることを確認 + expect(adB2cService.changePassword).toBeCalledTimes(1); + // loggerSpyがスパイしているlogger.logメソッドが出力したログを確認(目視確認用) + const logs = loggerSpy.mock.calls.map((call) => call[0]); + // 手動復旧が必要なエラーログが出力されていること + expect(logs.some((x) => x.startsWith(MANUAL_RECOVERY_REQUIRED))).toBe(true); }); }); @@ -467,6 +830,8 @@ describe('UsersService.createUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1448,6 +1813,8 @@ describe('UsersService.getUsers', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1937,6 +2304,8 @@ describe('UsersService.updateUser', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2533,6 +2902,8 @@ describe('UsersService.updateAcceptedVersion', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2633,6 +3004,8 @@ describe('UsersService.getUserName', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2687,6 +3060,8 @@ describe('UsersService.getRelations', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2878,3 +3253,1894 @@ describe('UsersService.getRelations', () => { } }); }); +describe('UsersService.deleteUser', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザーを削除できる(Author)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR_1', + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + + // ユーザーが削除されたことを確認 + { + const user = await getUser(source, user1); + expect(user).toBeNull(); + // ユーザアーカイブが作成されたことを確認 + const userArchive = await getUserArchive(source); + expect(userArchive[0].external_id).toBe(external_id); + } + }); + it('ユーザーを削除できる(Typist)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.TYPIST, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + + // ユーザーが削除されたことを確認 + { + const user = await getUser(source, user1); + expect(user).toBeNull(); + // ユーザアーカイブが作成されたことを確認 + const userArchive = await getUserArchive(source); + expect(userArchive[0].external_id).toBe(external_id); + } + },600000); + it('存在しないユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + + try { + await service.deleteUser(context, 100, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014001')); + } else { + fail(); + } + } + }); + it('管理者ユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + // 現在日付を作成 + const now = new Date(); + + try { + await service.deleteUser(context, admin.id, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014002')); + } else { + fail(); + } + } + }); + it('WorkFlowに割り当てられているユーザ(Author)は削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + const worktype1 = await createWorktype( + source, + account.id, + 'worktype1', + undefined, + true, + ); + await createOptionItems(source, worktype1.id); + await createWorkflow(source, account.id, user1, worktype1.id); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014003')); + } else { + fail(); + } + } + }); + it('WorkFlowに割り当てられているユーザ(Typist)は削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + const worktype1 = await createWorktype( + source, + account.id, + 'worktype1', + undefined, + true, + ); + await createOptionItems(source, worktype1.id); + const workflow1 = await createWorkflow( + source, + account.id, + user1, + worktype1.id, + ); + await createWorkflowTypist(source, workflow1.id, user2); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014004')); + } else { + fail(); + } + } + }); + it('ユーザグループに所属しているユーザは削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // ユーザグループを作成 + const userGroup = await createUserGroup(source, account.id, 'userGroup', [ + user1, + user2, + ]); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014005')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがInprogress)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.IN_PROGRESS, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)のAuthorIDが設定されているタスクがある場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + admin.id, // 作成者がadmin + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがInprogress)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.IN_PROGRESS, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Author)が有効なタスクをまだ持っている場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // タスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + user1, + ); + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user1, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014006')); + } else { + fail(); + } + } + }); + it('削除対象ユーザー(Typist)タスクのチェックアウト権限まだ持っている場合、削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // CheckoutPermissionを作成 + await createCheckoutPermissions(source, 100, user2); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + }); + + it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがFinished)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // user2が文字起こし担当のタスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.FINISHED, + user2, + 'AUTHOR', + user1, + ); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + },600000); + it('削除対象ユーザー(Typist)が文字起こし担当のタスクがまだ持っている場合、削除できない(statusがBackup)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // user2が文字起こし担当のタスクを作成 + await createTask( + source, + account.id, + 'task-url', + 'filename', + TASK_STATUS.BACKUP, + user2, + 'AUTHOR', + user1, + ); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014009')); + } else { + fail(); + } + } + }); + it('削除対象ユーザーが有効なライセンスをまだ持っている場合、削除できない', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account, admin } = await makeTestAccount(source, { + tier: 5, + }); + const { id: user1, external_id } = await makeTestUser(source, { + account_id: account.id, + role: USER_ROLES.AUTHOR, + author_id: 'AUTHOR', + }); + const { id: user2, external_id: external_id2 } = await makeTestUser( + source, + { + account_id: account.id, + role: USER_ROLES.TYPIST, + }, + ); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + // 明日まで有効なライセンスを作成して紐づける + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + await createLicense(source, account.id, user2, tomorrow); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + { + id: external_id, + displayName: 'user1', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user1@example.com', + }, + ], + }, + { + id: external_id2, + displayName: 'user2', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'user2@example.com', + }, + ], + }, + ]; + }, + getUser: async () => { + return { + id: admin.external_id, + displayName: 'admin', + }; + }, + }); + overrideSendgridService(service, {}); + + try { + // 現在日付を作成 + const now = new Date(); + await service.deleteUser(context, user2, now); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST); + expect(e.getResponse()).toEqual(makeErrorResponse('E014007')); + } else { + fail(); + } + } + }); +}); + +describe('UsersService.multipleImports', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザー一括登録ができる', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + overrideBlobstorageService(service, { + uploadImportsBlob: async () => {}, + }); + + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU120') + .mockImplementation(); + + const now = new Date(); + const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`; + const filename = 'fileName.csv'; + + const users = [ + { + name: 'name1', + email: 'mail1@example.com', + role: 1, + authorId: 'HOGE1', + autoRenew: 0, + notification: 0, + encryption: 0, + encryptionPassword: 'string', + prompt: 0, + }, + { + name: 'name2', + email: 'mail2@example.com', + role: 2, + autoRenew: 0, + notification: 0, + }, + ]; + await service.multipleImports(context, admin.external_id, filename, users); + + // メール送信メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + date, + filename, + ); + }); + + it('Blobアップロードに失敗した場合はエラーとなること', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + overrideBlobstorageService(service, { + uploadImportsBlob: async () => { + throw new Error('error'); + }, + }); + + const now = new Date(); + const date = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`; + const filename = 'fileName.csv'; + + const users = [ + { + name: 'name1', + email: 'mail1@example.com', + role: 1, + authorId: 'HOGE1', + autoRenew: 0, + notification: 0, + encryption: 0, + encryptionPassword: 'string', + prompt: 0, + }, + { + name: 'name2', + email: 'mail2@example.com', + role: 2, + autoRenew: 0, + notification: 0, + }, + ]; + + try { + await service.multipleImports( + context, + admin.external_id, + filename, + users, + ); + fail(); + } catch (e) { + if (e instanceof HttpException) { + expect(e.getStatus()).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(e.getResponse()).toEqual(makeErrorResponse('E009999')); + } else { + fail(); + } + } + }); +}); + +describe('UsersService.multipleImportsComplate', () => { + let source: DataSource | null = null; + + beforeAll(async () => { + if (source == null) { + source = await (async () => { + const s = new DataSource({ + type: 'mysql', + host: 'test_mysql_db', + port: 3306, + username: 'user', + password: 'password', + database: 'odms', + entities: [__dirname + '/../../**/*.entity{.ts,.js}'], + synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, + }); + return await s.initialize(); + })(); + } + }); + + beforeEach(async () => { + if (source) { + await truncateAllTable(source); + } + }); + + afterAll(async () => { + await source?.destroy(); + source = null; + }); + + it('ユーザー一括登録完了メールを送信できる(成功)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU121') + .mockImplementation(); + + const requestTime = Math.floor(new Date(2024, 2, 3).getTime() / 1000); + const filename = `U_20240303_000000_${admin.account_id}_${admin.id}.csv`; + const errors: MultipleImportErrors[] = []; + + // ユーザー一括登録完了 + await service.multipleImportsComplate( + context, + account.id, + filename, + requestTime, + errors, + ); + + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + '2024.3.3', + filename, + ); + }); + + it('ユーザー一括登録完了メールを送信できる(失敗)', async () => { + if (!source) fail(); + const module = await makeTestingModule(source); + if (!module) fail(); + + const { account: dealer } = await makeTestAccount(source, { + company_name: 'dealerCompany', + tier: 4, + }); + const { account, admin } = await makeTestAccount(source, { + tier: 5, + company_name: 'company', + parent_account_id: dealer.id, + }); + + const service = module.get(UsersService); + const context = makeContext(`uuidv4`, 'requestId'); + + overrideAdB2cService(service, { + getUsers: async () => { + return [ + { + id: admin.external_id, + displayName: 'admin', + identities: [ + { + signInType: ADB2C_SIGN_IN_TYPE.EMAILADDRESS, + issuer: 'issuer', + issuerAssignedId: 'admin@example.com', + }, + ], + }, + ]; + }, + }); + overrideSendgridService(service, {}); + // メール送信関数が呼ばれたかどうかで判定を行う。実際のメール送信は行わない。 + const spy = jest + .spyOn(service['sendgridService'], 'sendMailWithU122') + .mockImplementation(); + + const requestTime = Math.floor(new Date(2024, 2, 3).getTime() / 1000); + const filename = `U_20240303_000000_${admin.account_id}_${admin.id}`; + const errors: MultipleImportErrors[] = [ + { + name: 'user1', + line: 1, + errorCode: 'E010301', + }, + { + name: 'user2', + line: 2, + errorCode: 'E010302', + }, + { + name: 'user2', + line: 3, + errorCode: 'E010302', + }, + { + name: 'user3', + line: 4, + errorCode: 'E009999', + }, + ]; + + // ユーザー一括登録完了 + await service.multipleImportsComplate( + context, + account.id, + filename, + requestTime, + errors, + ); + + // ADB2Cユーザー削除メソッドが呼ばれているか確認 + expect(spy).toHaveBeenCalledWith( + context, + ['admin@example.com'], + account.company_name, + dealer.company_name, + [1], + [2, 3], + [4], + ); + }); +}); diff --git a/dictation_server/src/features/users/users.service.ts b/dictation_server/src/features/users/users.service.ts index 6b58912..ecea95d 100644 --- a/dictation_server/src/features/users/users.service.ts +++ b/dictation_server/src/features/users/users.service.ts @@ -23,11 +23,23 @@ import { } from '../../repositories/users/entity/user.entity'; import { UsersRepositoryService } from '../../repositories/users/users.repository.service'; import { LicensesRepositoryService } from '../../repositories/licenses/licenses.repository.service'; -import { GetRelationsResponse, User } from './types/types'; import { + MultipleImportUser, + GetRelationsResponse, + MultipleImportErrors, + User, +} from './types/types'; +import { + AdminDeleteFailedError, + AssignedWorkflowWithAuthorDeleteFailedError, + AssignedWorkflowWithTypistDeleteFailedError, AuthorIdAlreadyExistsError, EmailAlreadyVerifiedError, EncryptionPasswordNeedError, + ExistsGroupMemberDeleteFailedError, + ExistsTaskDeleteFailedError, + ExistsValidCheckoutDeleteFailedError, + ExistsValidLicenseDeleteFailedError, InvalidRoleChangeError, UpdateTermsVersionNotSetError, UserNotFoundError, @@ -51,13 +63,11 @@ import { import { AccountNotFoundError } from '../../repositories/accounts/errors/types'; import { getUserNameAndMailAddress } from '../../gateways/adb2c/utils/utils'; import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service'; -import { Account } from '../../repositories/accounts/entity/account.entity'; +import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service'; @Injectable() export class UsersService { private readonly logger = new Logger(UsersService.name); - private readonly mailFrom: string; - private readonly appDomain: string; constructor( private readonly accountsRepository: AccountsRepositoryService, private readonly usersRepository: UsersRepositoryService, @@ -66,10 +76,8 @@ export class UsersService { private readonly adB2cService: AdB2cService, private readonly configService: ConfigService, private readonly sendgridService: SendGridService, - ) { - this.mailFrom = this.configService.getOrThrow('MAIL_FROM'); - this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); - } + private readonly blobStorageService: BlobstorageService, + ) {} /** * Confirms user @@ -186,7 +194,6 @@ export class UsersService { ); //DBよりアクセス者の所属するアカウントIDを取得する let adminUser: EntityUser; - let account: Account | null; try { adminUser = await this.usersRepository.findUserByExternalId( context, @@ -201,7 +208,7 @@ export class UsersService { } const accountId = adminUser.account_id; - account = adminUser.account; + const account = adminUser.account; //authorIdが重複していないかチェックする if (authorId) { @@ -286,7 +293,7 @@ export class UsersService { this.logger.error(`[${context.getTrackingId()}]create user failed`); //リカバリー処理 //Azure AD B2Cに登録したユーザー情報を削除する - await this.deleteB2cUser(externalUser.sub, context); + await this.internalDeleteB2cUser(externalUser.sub, context); switch (e.code) { case 'ER_DUP_ENTRY': @@ -337,9 +344,9 @@ export class UsersService { this.logger.error(`[${context.getTrackingId()}] create user failed`); //リカバリー処理 //Azure AD B2Cに登録したユーザー情報を削除する - await this.deleteB2cUser(externalUser.sub, context); + await this.internalDeleteB2cUser(externalUser.sub, context); // DBからユーザーを削除する - await this.deleteUser(newUser.id, context); + await this.internalDeleteUser(newUser.id, context); throw new HttpException( makeErrorResponse('E009999'), HttpStatus.INTERNAL_SERVER_ERROR, @@ -353,10 +360,13 @@ export class UsersService { // Azure AD B2Cに登録したユーザー情報を削除する // TODO 「タスク 2452: リトライ処理を入れる箇所を検討し、実装する」の候補 - private async deleteB2cUser(externalUserId: string, context: Context) { + private async internalDeleteB2cUser( + externalUserId: string, + context: Context, + ) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ - this.deleteB2cUser.name + this.internalDeleteB2cUser.name } | params: { externalUserId: ${externalUserId} }`, ); try { @@ -371,16 +381,16 @@ export class UsersService { ); } finally { this.logger.log( - `[OUT] [${context.getTrackingId()}] ${this.deleteB2cUser.name}`, + `[OUT] [${context.getTrackingId()}] ${this.internalDeleteB2cUser.name}`, ); } } // DBに登録したユーザー情報を削除する - private async deleteUser(userId: number, context: Context) { + private async internalDeleteUser(userId: number, context: Context) { this.logger.log( `[IN] [${context.getTrackingId()}] ${ - this.deleteUser.name + this.internalDeleteUser.name } | params: { userId: ${userId} }`, ); try { @@ -393,7 +403,7 @@ export class UsersService { ); } finally { this.logger.log( - `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`, + `[OUT] [${context.getTrackingId()}] ${this.internalDeleteUser.name}`, ); } } @@ -506,46 +516,10 @@ export class UsersService { ); } - // ランダムなパスワードを生成する - const ramdomPassword = makePassword(); const { accountId, userId, email } = decodedToken; - try { - // ユーザー情報からAzure AD B2CのIDを特定する - const user = await this.usersRepository.findUserById(context, userId); - const extarnalId = user.external_id; - // パスワードを変更する - await this.adB2cService.changePassword( - context, - extarnalId, - ramdomPassword, - ); // ユーザを認証済みにする await this.usersRepository.updateUserVerified(context, userId); - - // メール送信処理 - try { - const { external_id: primaryAdminUserExternalId } = - await this.getPrimaryAdminUser(context, accountId); - - const adb2cUser = await this.adB2cService.getUser( - context, - primaryAdminUserExternalId, - ); - - const { displayName: primaryAdminName } = - getUserNameAndMailAddress(adb2cUser); - - await this.sendgridService.sendMailWithU113( - context, - email, - primaryAdminName, - ramdomPassword, - ); - } catch (e) { - this.logger.error(`[${context.getTrackingId()}] error=${e}`); - // メール送信に関する例外はログだけ出して握りつぶす - } } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); if (e instanceof Error) { @@ -562,6 +536,62 @@ export class UsersService { ); } } + } + + // ランダムなパスワードを生成する + const ramdomPassword = makePassword(); + try { + // ユーザー情報からAzure AD B2CのIDを特定する + const user = await this.usersRepository.findUserById(context, userId); + const extarnalId = user.external_id; + // パスワードを変更する + await this.adB2cService.changePassword( + context, + extarnalId, + ramdomPassword, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + this.logger.error( + `[${context.getTrackingId()}] change password failed. userId=${userId}`, + ); + // リカバリー処理 + // ユーザを未認証に戻す + await this.updateUserUnverified(context, userId); + + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + // メール送信処理 + try { + const { external_id: primaryAdminUserExternalId } = + await this.getPrimaryAdminUser(context, accountId); + + const adb2cUser = await this.adB2cService.getUser( + context, + primaryAdminUserExternalId, + ); + + const { displayName: primaryAdminName } = + getUserNameAndMailAddress(adb2cUser); + + await this.sendgridService.sendMailWithU113( + context, + email, + primaryAdminName, + ramdomPassword, + ); + } catch (e) { + // リカバリー処理 + // ユーザーを未認証に戻す + await this.updateUserUnverified(context, userId); + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${ @@ -1287,6 +1317,428 @@ export class UsersService { ); } } + /** + * ユーザーを削除する + * @param context + * @param extarnalId + * @param currentTime ライセンス有効期限のチェックに使用する現在時刻 + * @returns user + */ + async deleteUser( + context: Context, + userId: number, + currentTime: Date, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.deleteUser.name + } | params: { userId: ${userId} };`, + ); + try { + // 削除対象のユーザーが存在するかを確認する + let user: EntityUser; + try { + user = await this.usersRepository.findUserById(context, userId); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + if (e instanceof Error) { + switch (e.constructor) { + case UserNotFoundError: + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + } + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + // 削除対象のユーザーが管理者かどうかを確認する + const admins = await this.usersRepository.findAdminUsers( + context, + user.account_id, + ); + const adminIds = admins.map((admin) => admin.id); + if (adminIds.includes(user.id)) { + throw new HttpException( + makeErrorResponse('E014002'), + HttpStatus.BAD_REQUEST, + ); + } + // Azure AD B2Cからユーザー情報(e-mail, name)を取得する + const adminExternalIds = admins.map((admin) => admin.external_id); + const externalIds = [user.external_id, ...adminExternalIds]; + // Azure AD B2CのRateLimit対策のため、ユーザー情報を一括取得する + const details = await this.adB2cService.getUsers(context, externalIds); + // 削除対象のユーザーがAzure AD B2Cに存在するかを確認する + const deleteTargetDetail = details.find( + (details) => details.id === user.external_id, + ); + if (deleteTargetDetail == null) { + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + // 管理者の情報が0件(=競合でアカウントが削除された場合等)の場合はエラーを返す + const adminDetails = details.filter((details) => + adminExternalIds.includes(details.id), + ); + if (adminDetails.length === 0) { + // 通常ユーザーが取得できていて管理者が取得できない事は通常ありえないため、汎用エラー + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + const { emailAddress } = getUserNameAndMailAddress(deleteTargetDetail); + // メールアドレスが設定されていない場合はエラーを返す + if (emailAddress == null) { + throw new Error(`emailAddress is null. externalId=${user.external_id}`); + } + // 管理者のメールアドレスを取得 + const { adminEmails } = await this.getAccountInformation( + context, + user.account_id, + ); + // プライマリ管理者を取得 + const { external_id: adminExternalId } = await this.getPrimaryAdminUser( + context, + user.account_id, + ); + const adb2cAdminUser = await this.adB2cService.getUser( + context, + adminExternalId, + ); + const { displayName: primaryAdminName } = + getUserNameAndMailAddress(adb2cAdminUser); + + let isSuccess = false; + try { + const result = await this.usersRepository.deleteUser( + context, + userId, + currentTime, + ); + isSuccess = result.isSuccess; + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + switch (e.constructor) { + case AdminDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014002'), + HttpStatus.BAD_REQUEST, + ); + case AssignedWorkflowWithAuthorDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014003'), + HttpStatus.BAD_REQUEST, + ); + case AssignedWorkflowWithTypistDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014004'), + HttpStatus.BAD_REQUEST, + ); + case ExistsGroupMemberDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014005'), + HttpStatus.BAD_REQUEST, + ); + case ExistsTaskDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014006'), + HttpStatus.BAD_REQUEST, + ); + case ExistsValidLicenseDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014007'), + HttpStatus.BAD_REQUEST, + ); + case ExistsValidCheckoutDeleteFailedError: + throw new HttpException( + makeErrorResponse('E014009'), + HttpStatus.BAD_REQUEST, + ); + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + // トランザクションレベルで厳密に削除が成功したかを判定する + if (!isSuccess) { + // 既に削除されている場合はエラーを返す + throw new HttpException( + makeErrorResponse('E014001'), + HttpStatus.BAD_REQUEST, + ); + } + try { + // 削除を実施したことが確定したので、Azure AD B2Cからユーザーを削除する + await this.adB2cService.deleteUser(user.external_id, context); + this.logger.log( + `[${context.getTrackingId()}] delete externalUser: ${ + user.external_id + } | params: { ` + `externalUserId: ${user.external_id}, };`, + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to delete externalUser: ${ + user.external_id + }`, + ); + } + // 削除を実施したことが確定したので、メール送信処理を実施する + try { + await this.sendgridService.sendMailWithU116( + context, + deleteTargetDetail.displayName, + emailAddress, + primaryAdminName, + adminEmails, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteUser.name}`, + ); + } + } + /** + * ユーザー一括登録完了メールを送信する + * @param context + * @param accountId + * @param fileName + * @param requestTime + * @param errors + * @returns imports complate + */ + async multipleImportsComplate( + context: Context, + accountId: number, + fileName: string, + requestTime: number, + errors: MultipleImportErrors[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.multipleImportsComplate.name + } | params: { accountId: ${accountId}, fileName: ${fileName}, requestTime: ${requestTime} };`, + ); + try { + const account = await this.accountsRepository.findAccountById( + context, + accountId, + ); + if (!account) { + throw new Error(`account not found. id=${accountId}`); + } + + const dealerId = account.parent_account_id; + let dealerName: string | null = null; + if (dealerId !== null) { + const { company_name } = await this.accountsRepository.findAccountById( + context, + dealerId, + ); + dealerName = company_name; + } + + // アカウント情報を取得 + const { companyName, adminEmails } = await this.getAccountInformation( + context, + accountId, + ); + + if (errors.length === 0) { + const requestTimeDate = new Date(requestTime * 1000); + + // 完了メールを通知する + await this.sendgridService.sendMailWithU121( + context, + adminEmails, + companyName, + dealerName, + `${requestTimeDate.getFullYear()}.${ + requestTimeDate.getMonth() + 1 + }.${requestTimeDate.getDate()}`, + fileName, + ); + } else { + const duplicateEmails: number[] = []; + const duplicateAuthorIds: number[] = []; + const otherErrors: number[] = []; + // エラーを仕分ける + for (const error of errors) { + switch (error.errorCode) { + // メールアドレス重複エラー + case 'E010301': + duplicateEmails.push(error.line); + break; + // AuthorID重複エラー + case 'E010302': + duplicateAuthorIds.push(error.line); + break; + // その他エラー + default: + otherErrors.push(error.line); + break; + } + } + + // エラーメールを通知する + await this.sendgridService.sendMailWithU122( + context, + adminEmails, + companyName, + dealerName, + duplicateEmails, + duplicateAuthorIds, + otherErrors, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${ + this.multipleImportsComplate.name + }`, + ); + } + } + + /** + * ユーザー一括登録用のファイルをBlobにアップロードする + * @param context + * @param externalId + * @param fileName + * @param users + * @returns imports + */ + async multipleImports( + context: Context, + externalId: string, + fileName: string, + users: MultipleImportUser[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.multipleImports.name + } | params: { externalId: ${externalId}, ` + + `fileName: ${fileName}, ` + + `users.length: ${users.length} };`, + ); + try { + // ユーザー情報を取得 + const user = await this.usersRepository.findUserByExternalId( + context, + externalId, + ); + + if (user == null) { + throw new Error(`user not found. externalId=${externalId}`); + } + + const now = new Date(); + // 日時を生成(YYYYMMDD_HHMMSS) + const dateTime = + `${now.getFullYear().toString().padStart(4, '0')}` + + `${(now.getMonth() + 1).toString().padStart(2, '0')}` + // 月は0から始まるため+1する + `${now.getDate().toString().padStart(2, '0')}` + + `_${now.getHours().toString().padStart(2, '0')}` + + `${now.getMinutes().toString().padStart(2, '0')}` + + `${now.getSeconds().toString().padStart(2, '0')}`; + + // ファイル名を生成(U_YYYYMMDD_HHMMSS_アカウントID_ユーザーID.json) + const jsonFileName = `U_${dateTime}_${user.account_id}_${user.id}.json`; + + // ユーザー情報をJSON形式に変換 + const usersJson = JSON.stringify({ + account_id: user.account_id, + user_id: user.id, + user_role: user.role, + external_id: user.external_id, + file_name: fileName, + date: Math.floor(now.getTime() / 1000), + data: users.map((user) => { + return { + name: user.name, + email: user.email, + role: user.role, + author_id: user.authorId, + auto_renew: user.autoRenew, + notification: user.notification, + encryption: user.encryption, + encryption_password: user.encryptionPassword, + prompt: user.prompt, + }; + }), + }); + + // Blobにファイルをアップロード(ユーザー一括登録用) + await this.blobStorageService.uploadImportsBlob( + context, + jsonFileName, + usersJson, + ); + + // 受付完了メールを送信 + try { + // アカウント・管理者情報を取得 + const { adminEmails, companyName } = await this.getAccountInformation( + context, + user.account_id, + ); + + // Dealer情報を取得 + const dealer = await this.accountsRepository.findParentAccount( + context, + user.account_id, + ); + const dealerName = dealer?.company_name ?? null; + + const now = new Date(); + // 日時を生成(YYYY.MM.DD) + const date = `${now.getFullYear()}.${ + now.getMonth() + 1 // 月は0から始まるため+1する + }.${now.getDate()}`; + + await this.sendgridService.sendMailWithU120( + context, + adminEmails, + companyName, + dealerName, + date, + fileName, + ); + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + // メール送信に関する例外はログだけ出して握りつぶす + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + switch (e.constructor) { + default: + throw new HttpException( + makeErrorResponse('E009999'), + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.multipleImports.name}`, + ); + } + } /** * アカウントIDを指定して、アカウント情報と管理者情報を取得する @@ -1359,4 +1811,36 @@ export class UsersService { return primaryAdmin; } + + /** + * ユーザーを未認証にする + * @param context + * @param userId + * @returns void + */ + private async updateUserUnverified( + context: Context, + userId: number, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.updateUserUnverified.name + } | params: { userId: ${userId} };`, + ); + try { + await this.usersRepository.updateUserUnverified(context, userId); + this.logger.log( + `[${context.getTrackingId()}] update user unverified: ${userId}`, + ); + } catch (error) { + this.logger.error(`[${context.getTrackingId()}] error=${error}`); + this.logger.error( + `${MANUAL_RECOVERY_REQUIRED} [${context.getTrackingId()}] Failed to update user unverified: ${userId}`, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.updateUserUnverified.name}`, + ); + } + } } diff --git a/dictation_server/src/features/workflows/workflows.service.spec.ts b/dictation_server/src/features/workflows/workflows.service.spec.ts index 5c464aa..1ff4fe7 100644 --- a/dictation_server/src/features/workflows/workflows.service.spec.ts +++ b/dictation_server/src/features/workflows/workflows.service.spec.ts @@ -20,6 +20,7 @@ import { WorkflowsRepositoryService } from '../../repositories/workflows/workflo import { HttpException, HttpStatus } from '@nestjs/common'; import { makeErrorResponse } from '../../common/error/makeErrorResponse'; import { truncateAllTable } from '../../common/test/init'; +import { TestLogger } from '../../common/test/logger'; describe('getWorkflows', () => { let source: DataSource | null = null; @@ -35,6 +36,8 @@ describe('getWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -292,6 +295,8 @@ describe('createWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -1231,6 +1236,8 @@ describe('updateWorkflow', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); @@ -2433,6 +2440,8 @@ describe('deleteWorkflows', () => { database: 'odms', entities: [__dirname + '/../../**/*.entity{.ts,.js}'], synchronize: false, // trueにすると自動的にmigrationが行われるため注意 + logger: new TestLogger('none'), + logging: true, }); return await s.initialize(); })(); diff --git a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts index 2381148..fcd8301 100644 --- a/dictation_server/src/gateways/blobstorage/blobstorage.service.ts +++ b/dictation_server/src/gateways/blobstorage/blobstorage.service.ts @@ -22,9 +22,11 @@ export class BlobstorageService { private readonly blobServiceClientUS: BlobServiceClient; private readonly blobServiceClientEU: BlobServiceClient; private readonly blobServiceClientAU: BlobServiceClient; + private readonly blobServiceClientImports: BlobServiceClient; private readonly sharedKeyCredentialUS: StorageSharedKeyCredential; private readonly sharedKeyCredentialAU: StorageSharedKeyCredential; private readonly sharedKeyCredentialEU: StorageSharedKeyCredential; + private readonly sharedKeyCredentialImports: StorageSharedKeyCredential; private readonly sasTokenExpireHour: number; constructor(private readonly configService: ConfigService) { this.sharedKeyCredentialUS = new StorageSharedKeyCredential( @@ -39,6 +41,10 @@ export class BlobstorageService { this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_EU'), this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_EU'), ); + this.sharedKeyCredentialImports = new StorageSharedKeyCredential( + this.configService.getOrThrow('STORAGE_ACCOUNT_NAME_IMPORTS'), + this.configService.getOrThrow('STORAGE_ACCOUNT_KEY_IMPORTS'), + ); this.blobServiceClientUS = new BlobServiceClient( this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_US'), this.sharedKeyCredentialUS, @@ -51,6 +57,10 @@ export class BlobstorageService { this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_EU'), this.sharedKeyCredentialEU, ); + this.blobServiceClientImports = new BlobServiceClient( + this.configService.getOrThrow('STORAGE_ACCOUNT_ENDPOINT_IMPORTS'), + this.sharedKeyCredentialImports, + ); this.sasTokenExpireHour = this.configService.getOrThrow( 'STORAGE_TOKEN_EXPIRE_TIME', ); @@ -93,7 +103,6 @@ export class BlobstorageService { ); } } - /** * 指定されたコンテナを削除します。(コンテナが存在しない場合、何もせず終了します) * @param context @@ -142,6 +151,60 @@ export class BlobstorageService { } } + /** + * 指定されたファイルを削除します。 + * @param context + * @param accountId + * @param country + * @param fileName + * @returns file + */ + async deleteFile( + context: Context, + accountId: number, + country: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.deleteFile.name} | params: { ` + + `accountId: ${accountId} ` + + `country: ${country} ` + + `fileName: ${fileName} };`, + ); + + try { + // 国に応じたリージョンでコンテナ名を指定してClientを取得 + const containerClient = this.getContainerClient( + context, + accountId, + country, + ); + // コンテナ内のBlobパス名を指定してClientを取得 + const blobClient = containerClient.getBlobClient(fileName); + + const { succeeded, errorCode, date } = await blobClient.deleteIfExists(); + this.logger.log( + `[${context.getTrackingId()}] succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`, + ); + + // 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする + // Blob不在の場合のエラーコードは「BlobNotFound」以下を参照 + // https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes + if (!succeeded && errorCode !== 'BlobNotFound') { + throw new Error( + `delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.deleteFile.name}`, + ); + } + } + /** * Containers exists * @param country @@ -403,6 +466,45 @@ export class BlobstorageService { return url.toString(); } + /** + * 一括登録用のBlobを作成します + * @param context + * @param fileName + * @param content + * @returns imports blob + */ + async uploadImportsBlob( + context: Context, + fileName: string, + content: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${ + this.uploadImportsBlob.name + } | params: { fileName: ${fileName} };`, + ); + + try { + const containerClient = + this.blobServiceClientImports.getContainerClient('import-users'); + const blockBlobClient = containerClient.getBlockBlobClient(fileName); + const result = await blockBlobClient.upload(content, content.length); + + if (result.errorCode) { + throw new Error( + `upload failed. errorCode: ${result.errorCode} fileName: ${fileName}`, + ); + } + } catch (e) { + this.logger.error(`[${context.getTrackingId()}] error=${e}`); + throw e; + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.uploadImportsBlob.name}`, + ); + } + } + /** * Gets container client * @param companyName diff --git a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts index cd3edc4..7432e72 100644 --- a/dictation_server/src/gateways/sendgrid/sendgrid.service.ts +++ b/dictation_server/src/gateways/sendgrid/sendgrid.service.ts @@ -20,6 +20,19 @@ import { TYPIST_NAME, VERIFY_LINK, TEMPORARY_PASSWORD, + EMAIL_DUPLICATION_EN, + AUTHOR_ID_DUPLICATION_EN, + UNEXPECTED_ERROR_EN, + EMAIL_DUPLICATION_DE, + AUTHOR_ID_DUPLICATION_DE, + UNEXPECTED_ERROR_DE, + EMAIL_DUPLICATION_FR, + AUTHOR_ID_DUPLICATION_FR, + UNEXPECTED_ERROR_FR, + REQUEST_TIME, + NO_ERROR_MESSAGE_EN, + NO_ERROR_MESSAGE_DE, + NO_ERROR_MESSAGE_FR, } from '../../templates/constants'; import { URL } from 'node:url'; @@ -59,8 +72,34 @@ export class SendGridService { private readonly templateU114Text: string; private readonly templateU115Html: string; private readonly templateU115Text: string; + private readonly templateU116Html: string; + private readonly templateU116Text: string; private readonly templateU117Html: string; private readonly templateU117Text: string; + private readonly templateU118Html: string; + private readonly templateU118Text: string; + private readonly templateU118NoParentHtml: string; + private readonly templateU118NoParentText: string; + private readonly templateU119Html: string; + private readonly templateU119Text: string; + private readonly templateU119NoParentHtml: string; + private readonly templateU119NoParentText: string; + private readonly templateU120Html: string; + private readonly templateU120Text: string; + private readonly templateU120NoParentHtml: string; + private readonly templateU120NoParentText: string; + private readonly templateU121Html: string; + private readonly templateU121Text: string; + private readonly templateU121NoParentHtml: string; + private readonly templateU121NoParentText: string; + private readonly templateU122Html: string; + private readonly templateU122Text: string; + private readonly templateU122NoParentHtml: string; + private readonly templateU122NoParentText: string; + private readonly templateU123Html: string; + private readonly templateU123Text: string; + private readonly templateU124Html: string; + private readonly templateU124Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); @@ -191,6 +230,14 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_115.txt`), 'utf-8', ); + this.templateU116Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_116.html`), + 'utf-8', + ); + this.templateU116Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_116.txt`), + 'utf-8', + ); this.templateU117Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_117.html`), 'utf-8', @@ -199,6 +246,117 @@ export class SendGridService { path.resolve(__dirname, `../../templates/template_U_117.txt`), 'utf-8', ); + this.templateU118Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118.html`), + 'utf-8', + ); + this.templateU118Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118.txt`), + 'utf-8', + ); + this.templateU118NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_118_no_parent.html`, + ), + 'utf-8', + ); + this.templateU118NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_118_no_parent.txt`), + 'utf-8', + ); + this.templateU119Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119.html`), + 'utf-8', + ); + this.templateU119Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119.txt`), + 'utf-8', + ); + this.templateU119NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_119_no_parent.html`, + ), + 'utf-8', + ); + this.templateU119NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_119_no_parent.txt`), + 'utf-8', + ); + this.templateU120Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120.html`), + 'utf-8', + ); + this.templateU120Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120.txt`), + 'utf-8', + ); + this.templateU120NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_120_no_parent.html`, + ), + 'utf-8', + ); + this.templateU120NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_120_no_parent.txt`), + 'utf-8', + ); + this.templateU121Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121.html`), + 'utf-8', + ); + this.templateU121Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121.txt`), + 'utf-8', + ); + this.templateU121NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_121_no_parent.html`, + ), + 'utf-8', + ); + this.templateU121NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_121_no_parent.txt`), + 'utf-8', + ); + this.templateU122Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122.html`), + 'utf-8', + ); + this.templateU122Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122.txt`), + 'utf-8', + ); + this.templateU122NoParentHtml = readFileSync( + path.resolve( + __dirname, + `../../templates/template_U_122_no_parent.html`, + ), + 'utf-8', + ); + this.templateU122NoParentText = readFileSync( + path.resolve(__dirname, `../../templates/template_U_122_no_parent.txt`), + 'utf-8', + ); + this.templateU123Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.html`), + 'utf-8', + ); + this.templateU123Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_123.txt`), + 'utf-8', + ); + this.templateU124Html = readFileSync( + path.resolve(__dirname, `../../templates/template_U_124.html`), + 'utf-8', + ); + this.templateU124Text = readFileSync( + path.resolve(__dirname, `../../templates/template_U_124.txt`), + 'utf-8', + ); } } @@ -862,6 +1020,53 @@ export class SendGridService { } } + /** + * U-116のテンプレートを使用したメールを送信する + * @param context + * @param userName 削除されたユーザーの名前 + * @param userMail 削除されたユーザーのメールアドレス + * @param primaryAdminName 削除されたユーザーの所属するアカウントの管理者(primary)の名前 + * @param adminMails 削除されたユーザーの所属するアカウントの管理者(primary/secondary)のメールアドレス + * @returns mail with u116 + */ + async sendMailWithU116( + context: Context, + userName: string, + userMail: string, + primaryAdminName: string, + adminMails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU116.name}`, + ); + try { + const subject = 'User Deleted Notification [U-116]'; + + // メールの本文を作成する + const html = this.templateU116Html + .replaceAll(USER_NAME, userName) + .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); + const text = this.templateU116Text + .replaceAll(USER_NAME, userName) + .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName); + + // メールを送信する + await this.sendMail( + context, + [userMail], + adminMails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU116.name}`, + ); + } + } + /** * U-117のテンプレートを使用したメールを送信する * @param context @@ -917,6 +1122,487 @@ export class SendGridService { } } + /** + * U-118のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @returns mail with u118 + */ + async sendMailWithU118( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU118.name}`, + ); + try { + const subject = 'Storage Usage Worning Notification [U-118]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU118NoParentHtml.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + text = this.templateU118NoParentText.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + } else { + html = this.templateU118Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + text = this.templateU118Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU118.name}`, + ); + } + } + + /** + * U-119のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param tire1AdminMails 第一階層の管理者(primary/secondary)のメールアドレス + * @returns mail with u119 + */ + async sendMailWithU119( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + tire1AdminMails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU119.name}`, + ); + try { + const subject = 'Storage Usage Exceeded Notification [U-119]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU119NoParentHtml.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + text = this.templateU119NoParentText.replaceAll( + CUSTOMER_NAME, + customerAccountName, + ); + } else { + html = this.templateU119Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + text = this.templateU119Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + tire1AdminMails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU119.name}`, + ); + } + } + + /** + * U-120のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param tire1AdminMails 第一階層の管理者(primary/secondary)のメールアドレス + * @returns mail with u119 + */ + async sendMailWithU120( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + requestTime: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`, + ); + try { + const subject = 'User Bulk Registration Received Notification [U-120]'; + + let html: string; + let text: string; + console.log(dealerAccountName); + + if (!dealerAccountName) { + html = this.templateU120NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU120NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } else { + html = this.templateU120Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU120Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU120.name}`, + ); + } + } + + /** + * U-121のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param requestTime ユーザー一括登録のリクエストを受け付けた日時 + * @param fileName ユーザー一括登録で使用されたファイル名 + * @returns mail with u121 + */ + async sendMailWithU121( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + requestTime: string, + fileName: string, + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU121.name}`, + ); + try { + const subject = 'User Bulk Registration Completed Notification [U-121]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU121NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU121NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } else { + html = this.templateU121Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + text = this.templateU121Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(REQUEST_TIME, requestTime) + .replaceAll(FILE_NAME, fileName); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU121.name}`, + ); + } + } + + /** + * U-122のテンプレートを使用したメールを送信する + * @param context + * @param customerAdminMails アカウントの管理者(primary/secondary)のメールアドレス + * @param customerAccountName アカウントの名前 + * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) + * @param duplicateEmails メールアドレスの重複エラーのある行番号 + * @param duplicateAuthorIds AuthorIdの重複エラーのある行番号 + * @param otherErrors その他のエラーのある行番号 + * @returns mail with u122 + */ + async sendMailWithU122( + context: Context, + customerAdminMails: string[], + customerAccountName: string, + dealerAccountName: string | null, + duplicateEmails: number[], + duplicateAuthorIds: number[], + otherErrors: number[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU122.name}`, + ); + try { + let duplicateEmailsMsgEn = NO_ERROR_MESSAGE_EN; + let duplicateAuthorIdsMsgEn = NO_ERROR_MESSAGE_EN; + let otherErrorsMsgEn = NO_ERROR_MESSAGE_EN; + let duplicateEmailsMsgDe = NO_ERROR_MESSAGE_DE; + let duplicateAuthorIdsMsgDe = NO_ERROR_MESSAGE_DE; + let otherErrorsMsgDe = NO_ERROR_MESSAGE_DE; + let duplicateEmailsMsgFr = NO_ERROR_MESSAGE_FR; + let duplicateAuthorIdsMsgFr = NO_ERROR_MESSAGE_FR; + let otherErrorsMsgFr = NO_ERROR_MESSAGE_FR; + + if (duplicateEmails.length !== 0) { + duplicateEmailsMsgEn = duplicateEmails.map((x) => `L${x}`).join(', '); + duplicateEmailsMsgDe = duplicateEmails.map((x) => `L${x}`).join(', '); + duplicateEmailsMsgFr = duplicateEmails.map((x) => `L${x}`).join(', '); + } + if (duplicateAuthorIds.length !== 0) { + duplicateAuthorIdsMsgEn = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + duplicateAuthorIdsMsgDe = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + duplicateAuthorIdsMsgFr = duplicateAuthorIds + .map((x) => `L${x}`) + .join(', '); + } + if (otherErrors.length !== 0) { + otherErrorsMsgEn = otherErrors.map((x) => `L${x}`).join(', '); + otherErrorsMsgDe = otherErrors.map((x) => `L${x}`).join(', '); + otherErrorsMsgFr = otherErrors.map((x) => `L${x}`).join(', '); + } + + const subject = 'User Bulk Registration Failed Notification [U-122]'; + + let html: string; + let text: string; + + if (!dealerAccountName) { + html = this.templateU122NoParentHtml + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); + text = this.templateU122NoParentText + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); + } else { + html = this.templateU122Html + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); + text = this.templateU122Text + .replaceAll(CUSTOMER_NAME, customerAccountName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(EMAIL_DUPLICATION_EN, duplicateEmailsMsgEn) + .replaceAll(EMAIL_DUPLICATION_DE, duplicateEmailsMsgDe) + .replaceAll(EMAIL_DUPLICATION_FR, duplicateEmailsMsgFr) + .replaceAll(AUTHOR_ID_DUPLICATION_EN, duplicateAuthorIdsMsgEn) + .replaceAll(AUTHOR_ID_DUPLICATION_DE, duplicateAuthorIdsMsgDe) + .replaceAll(AUTHOR_ID_DUPLICATION_FR, duplicateAuthorIdsMsgFr) + .replaceAll(UNEXPECTED_ERROR_EN, otherErrorsMsgEn) + .replaceAll(UNEXPECTED_ERROR_DE, otherErrorsMsgDe) + .replaceAll(UNEXPECTED_ERROR_FR, otherErrorsMsgFr); + } + + // メールを送信する + await this.sendMail( + context, + customerAdminMails, + [], + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU122.name}`, + ); + } + } + + /** + * U-123のテンプレートを使用したメールを送信する + * @param context + * @param partnerAccountName + * @param partnerPrimaryName + * @param partnerPrimaryMail + * @param dealerAccountName + * @param dealerEmails + * @returns mail with u123 + */ + async sendMailWithU123( + context: Context, + partnerAccountName: string, + partnerPrimaryName: string, + partnerPrimaryMail: string, + dealerAccountName: string, + dealerEmails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + try { + const subject = 'Partner Account Deleted Notification [U-123]'; + + const html = this.templateU123Html + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + const text = this.templateU123Text + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName); + + // メールを送信する + await this.sendMail( + context, + [partnerPrimaryMail], + dealerEmails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU123.name}`, + ); + } + } + + /** + * U-124のテンプレートを使用したメールを送信する + * @param context + * @param partnerAccountName + * @param partnerPrimaryName + * @param partnerPrimaryMail + * @param dealerAccountName + * @param dealerEmails + * @returns mail with u124 + */ + async sendMailWithU124( + context: Context, + partnerAccountName: string, + partnerPrimaryName: string, + partnerPrimaryMail: string, + dealerAccountName: string, + dealerEmails: string[], + ): Promise { + this.logger.log( + `[IN] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`, + ); + try { + const subject = 'Partner Account Edit Notification [U-124]'; + const url = new URL(this.appDomain).href; + + const html = this.templateU124Html + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(TOP_URL, url); + const text = this.templateU124Text + .replaceAll(CUSTOMER_NAME, partnerAccountName) + .replaceAll(PRIMARY_ADMIN_NAME, partnerPrimaryName) + .replaceAll(DEALER_NAME, dealerAccountName) + .replaceAll(TOP_URL, url); + + // メールを送信する + await this.sendMail( + context, + [partnerPrimaryMail], + dealerEmails, + this.mailFrom, + subject, + text, + html, + ); + } finally { + this.logger.log( + `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU124.name}`, + ); + } + } + /** * メールを送信する * @param context diff --git a/dictation_server/src/repositories/accounts/accounts.repository.module.ts b/dictation_server/src/repositories/accounts/accounts.repository.module.ts index 999d78a..1ce9003 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.module.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.module.ts @@ -3,9 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Account } from './entity/account.entity'; import { AccountsRepositoryService } from './accounts.repository.service'; import { License, LicenseOrder } from '../licenses/entity/license.entity'; +import { AccountArchive } from './entity/account_archive.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Account, License, LicenseOrder])], + imports: [ + TypeOrmModule.forFeature([Account, AccountArchive, License, LicenseOrder]), + ], providers: [AccountsRepositoryService], exports: [AccountsRepositoryService], }) diff --git a/dictation_server/src/repositories/accounts/accounts.repository.service.ts b/dictation_server/src/repositories/accounts/accounts.repository.service.ts index 2832672..06ea6ef 100644 --- a/dictation_server/src/repositories/accounts/accounts.repository.service.ts +++ b/dictation_server/src/repositories/accounts/accounts.repository.service.ts @@ -26,6 +26,7 @@ import { getTaskListSortableAttribute, } from '../../common/types/sort/util'; import { + INITIAL_JOB_NUMBER, LICENSE_ALLOCATED_STATUS, LICENSE_EXPIRATION_THRESHOLD_DAYS, LICENSE_ISSUE_STATUS, @@ -35,6 +36,8 @@ import { AccountNotFoundError, AdminUserNotFoundError, DealerAccountNotFoundError, + HierarchyMismatchError, + PartnerAccountDeletionError, } from './errors/types'; import { AlreadyLicenseAllocatedError, @@ -64,6 +67,8 @@ import { PartnerInfoFromDb, PartnerLicenseInfoForRepository, } from '../../features/accounts/types/types'; +import { AccountArchive } from './entity/account_archive.entity'; +import { JobNumber } from '../job_number/entity/job_number.entity'; @Injectable() export class AccountsRepositoryService { @@ -169,7 +174,6 @@ export class AccountsRepositoryService { this.isCommentOut, context, ); - // 作成されたAccountのIDを使用してユーザーを作成 const user = new User(); { @@ -207,6 +211,20 @@ export class AccountsRepositoryService { throw new Error(`invalid update. result.affected=${result.affected}`); } + // job_numberの初期値を設定 + const jobNumberRepo = entityManager.getRepository(JobNumber); + const initialJobNumber = jobNumberRepo.create({ + account_id: persistedAccount.id, + job_number: INITIAL_JOB_NUMBER, + }); + await insertEntity( + JobNumber, + jobNumberRepo, + initialJobNumber, + this.isCommentOut, + context, + ); + // ユーザーのタスクソート条件を作成 const sortCriteria = new SortCriteria(); { @@ -242,6 +260,14 @@ export class AccountsRepositoryService { const accountsRepo = entityManager.getRepository(Account); const usersRepo = entityManager.getRepository(User); const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + const jobNumberRepo = entityManager.getRepository(JobNumber); + // JobNumberを削除 + await deleteEntity( + jobNumberRepo, + { account_id: accountId }, + this.isCommentOut, + context, + ); // ソート条件を削除 await deleteEntity( sortCriteriaRepo, @@ -280,6 +306,80 @@ export class AccountsRepositoryService { return account; } + /** + * 指定されたアカウントIDのアカウント一覧を取得する + * @param context + * @param ids + * @returns accounts by id + */ + async findAccountsById(context: Context, ids: number[]): Promise { + const accounts = await this.dataSource.getRepository(Account).find({ + where: { + id: In(ids), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + return accounts; + } + + /** + * OMDSTokyoのアカウント情報を取得する + * @param id + * @returns account + */ + async findTier1Account(context: Context): Promise { + const account = await this.dataSource.getRepository(Account).findOne({ + where: { + tier: TIERS.TIER1, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!account) { + throw new AccountNotFoundError(`Account is Not Found.`); + } + return account; + } + + /** + * 指定したアカウントの親アカウント情報を取得します(存在しない場合はnullを返します) + * @param context + * @param accountId + * @returns parent account + */ + async findParentAccount( + context: Context, + accountId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountsRepo = entityManager.getRepository(Account); + const myAccount = await accountsRepo.findOne({ + where: { + id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!myAccount) { + throw new AccountNotFoundError(`Target Account is Not Found.`); + } + + if (!myAccount.parent_account_id) { + // 親アカウントが存在しない場合は明示的にnullを返す + return null; + } + + const parentAccount = await accountsRepo.findOne({ + where: { + id: myAccount.parent_account_id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + return parentAccount; + }); + } + /** * ※サブルーチンとして、別途トランザクション開始された処理から呼び出されることを想定 * 有効期限が現在日付からしきい値以内のライセンス数を取得する @@ -398,37 +498,21 @@ export class AccountsRepositoryService { // 有効な総ライセンス数のうち、ユーザーに割り当て済みのライセンス数を取得する const allocatedLicense = await license.count({ - where: [ - { - account_id: id, - allocated_user_id: Not(IsNull()), - expiry_date: MoreThanOrEqual(currentDate), - status: LICENSE_ALLOCATED_STATUS.ALLOCATED, - }, - { - account_id: id, - allocated_user_id: Not(IsNull()), - expiry_date: IsNull(), - status: LICENSE_ALLOCATED_STATUS.ALLOCATED, - }, - ], + where: { + account_id: id, + expiry_date: MoreThanOrEqual(currentDate), + status: LICENSE_ALLOCATED_STATUS.ALLOCATED, + }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); // 総ライセンス数のうち、ユーザーに割り当てたことがあるが、現在は割り当て解除され誰にも割り当たっていないライセンス数を取得する const reusableLicense = await license.count({ - where: [ - { - account_id: id, - expiry_date: MoreThanOrEqual(currentDate), - status: LICENSE_ALLOCATED_STATUS.REUSABLE, - }, - { - account_id: id, - expiry_date: IsNull(), - status: LICENSE_ALLOCATED_STATUS.REUSABLE, - }, - ], + where: { + account_id: id, + expiry_date: MoreThanOrEqual(currentDate), + status: LICENSE_ALLOCATED_STATUS.REUSABLE, + }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, }); @@ -663,8 +747,8 @@ export class AccountsRepositoryService { ); // 第五の不足数を算出するためのライセンス数情報を取得する - let expiringSoonLicense: number = 0; - let allocatableLicenseWithMargin: number = 0; + let expiringSoonLicense: number = 0; // eslint-disable-line + let allocatableLicenseWithMargin: number = 0; // eslint-disable-line if (childAccount.tier === TIERS.TIER5) { expiringSoonLicense = await this.getExpiringSoonLicense( context, @@ -959,11 +1043,11 @@ export class AccountsRepositoryService { /** * 一階層上のアカウントを取得する - * @param accountId - * @param tier + * @param accountId 自身の一階層上のアカウントID(子アカウントではない) + * @param tier 自身のアカウントの階層 * @returns account: 一階層上のアカウント */ - async getOneUpperTierAccount( + private async getOneUpperTierAccount( context: Context, accountId: number, tier: number, @@ -1116,6 +1200,23 @@ export class AccountsRepositoryService { accountId: number, ): Promise { return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のアカウントを退避テーブルに退避 + const accountRepo = entityManager.getRepository(Account); + const account = await accountRepo.find({ + where: { + id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const accountArchiveRepo = entityManager.getRepository(AccountArchive); + await insertEntities( + AccountArchive, + accountArchiveRepo, + account, + this.isCommentOut, + context, + ); + // 削除対象のユーザーを退避テーブルに退避 const users = await this.dataSource.getRepository(User).find({ where: { @@ -1168,8 +1269,16 @@ export class AccountsRepositoryService { context, ); + // jobNumberのテーブルのレコードを削除する + const jobNumberRepo = entityManager.getRepository(JobNumber); + await deleteEntity( + jobNumberRepo, + { account_id: accountId }, + this.isCommentOut, + context, + ); + // アカウントを削除 - const accountRepo = entityManager.getRepository(Account); await deleteEntity( accountRepo, { id: accountId }, @@ -1335,4 +1444,441 @@ export class AccountsRepositoryService { return users; }); } + + /* + * 自動ファイル削除に関する項目を更新する + * @param accountId + * @param isAutoFileDelete + * @param retentionDays + */ + async updateFileDeleteSetting( + context: Context, + accountId: number, + isAutoFileDelete: boolean, + retentionDays: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // アカウントが存在するかチェック + const account = await accountRepo.findOne({ + where: { id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // アカウントが存在しない場合はエラー + if (!account) { + throw new AccountNotFoundError( + `Account is not found. id: ${accountId}`, + ); + } + + // 自動ファイル削除設定の更新を行う + await updateEntity( + accountRepo, + { id: accountId }, + { + auto_file_delete: isAutoFileDelete, + file_retention_days: retentionDays, + }, + this.isCommentOut, + context, + ); + }); + } + + /** + * 指定したアカウントのシステム利用制限状態を更新する + * @param context + * @param accountId 更新対象アカウントID + * @param restricted 制限するかどうか(true:制限する) + */ + async updateRestrictionStatus( + context: Context, + accountId: number, + restricted: boolean, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + + // アカウントが存在するかチェック + const account = await accountRepo.findOne({ + where: { id: accountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // アカウントが存在しない場合はエラー + if (!account) { + throw new AccountNotFoundError( + `Account is not found. id: ${accountId}`, + ); + } + + // アカウント利用制限状態の更新を行う + await updateEntity( + accountRepo, + { id: accountId }, + { + locked: restricted, + }, + this.isCommentOut, + context, + ); + }); + } + + /** + * アカウント階層構造変更とそれに伴う処理を行う。 + * @param context + * @param newParent + * @param children + * @returns parent account + */ + async switchParentAccount( + context: Context, + newParent: number, + children: number[], + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + const childrenAccounts = await accountRepo.find({ + where: { + id: In(children), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + accountRepo, + { id: In(childrenAccounts.map((child) => child.id)) }, + { + parent_account_id: newParent, + delegation_permission: false, + }, + this.isCommentOut, + context, + ); + + const licenseOrderRepo = entityManager.getRepository(LicenseOrder); + const cancelTargets = await licenseOrderRepo.find({ + where: { + from_account_id: In(children), + status: LICENSE_ISSUE_STATUS.ISSUE_REQUESTING, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + await updateEntity( + licenseOrderRepo, + { id: In(cancelTargets.map((cancelTarget) => cancelTarget.id)) }, + { + status: LICENSE_ISSUE_STATUS.CANCELED, + canceled_at: new Date(), + }, + this.isCommentOut, + context, + ); + }); + } + + /** + * 指定したパートナーアカウントを削除する + * @param context + * @param parentAccountId + * @param targetAccountId + * @returns partner account + */ + async deletePartnerAccount( + context: Context, + parentAccountId: number, + targetAccountId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + // 削除対象のユーザーを取得 + const userRepo = entityManager.getRepository(User); + const users = await userRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const accountRepo = entityManager.getRepository(Account); + + // 対象アカウントが存在するかチェック + const targetAccount = await accountRepo.findOne({ + where: { id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!targetAccount) { + throw new AccountNotFoundError( + `Account is not found. id: ${targetAccountId}`, + ); + } + + // 実行者のアカウントが対象アカウントの親アカウントでない場合はエラー + if (targetAccount.parent_account_id !== parentAccountId) { + throw new PartnerAccountDeletionError( + `Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`, + ); + } + + // 対象アカウントに子アカウントが存在する場合はエラー + const childrenAccounts = await accountRepo.find({ + where: { parent_account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // 子アカウントが存在する場合はエラー + if (childrenAccounts.length > 0) { + throw new PartnerAccountDeletionError( + `Target account has children account. targetAccountId: ${targetAccountId}`, + ); + } + + // ユーザテーブルのレコードを削除する + await deleteEntity( + userRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ソート条件のテーブルのレコードを削除する + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + await deleteEntity( + sortCriteriaRepo, + { user_id: In(users.map((user) => user.id)) }, + this.isCommentOut, + context, + ); + + // JobNumberのテーブルのレコードを削除する + const jobNumberRepo = entityManager.getRepository(JobNumber); + await deleteEntity( + jobNumberRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // アカウントを削除 + await deleteEntity( + accountRepo, + { id: targetAccountId }, + this.isCommentOut, + context, + ); + // ライセンス系(card_license_issue以外)のテーブルのレコードを削除する + const orderRepo = entityManager.getRepository(LicenseOrder); + await deleteEntity( + orderRepo, + { from_account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseRepo = entityManager.getRepository(License); + const targetLicenses = await licenseRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const cardLicenseRepo = entityManager.getRepository(CardLicense); + await deleteEntity( + cardLicenseRepo, + { license_id: In(targetLicenses.map((license) => license.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + licenseRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + await deleteEntity( + licenseAllocationHistoryRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ユーザーグループ系のテーブルのレコードを削除する + const userGroupRepo = entityManager.getRepository(UserGroup); + const targetUserGroup = await userGroupRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + await deleteEntity( + userGroupMemberRepo, + { + user_group_id: In(targetUserGroup.map((userGroup) => userGroup.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + userGroupRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // ワークタイプ系のテーブルのレコードを削除する + const worktypeRepo = entityManager.getRepository(Worktype); + const taggerWorktypes = await worktypeRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + const optionItemRepo = entityManager.getRepository(OptionItem); + await deleteEntity( + optionItemRepo, + { worktype_id: In(taggerWorktypes.map((worktype) => worktype.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + worktypeRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // テンプレートファイルテーブルのレコードを削除する + const templateFileRepo = entityManager.getRepository(TemplateFile); + await deleteEntity( + templateFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // オーディオファイル系のテーブルのレコードを削除する + const audioFileRepo = entityManager.getRepository(AudioFile); + const targetaudioFiles = await audioFileRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const audioOptionItemsRepo = entityManager.getRepository(AudioOptionItem); + await deleteEntity( + audioOptionItemsRepo, + { + audio_file_id: In(targetaudioFiles.map((audioFile) => audioFile.id)), + }, + this.isCommentOut, + context, + ); + await deleteEntity( + audioFileRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + // タスク系のテーブルのレコードを削除する + const taskRepo = entityManager.getRepository(Task); + const targetTasks = await taskRepo.find({ + where: { account_id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + await deleteEntity( + checkoutPermissionRepo, + { task_id: In(targetTasks.map((task) => task.id)) }, + this.isCommentOut, + context, + ); + await deleteEntity( + taskRepo, + { account_id: targetAccountId }, + this.isCommentOut, + context, + ); + + return users; + }); + } + /** + * 指定したパートナーアカウントの情報を更新する + * @param context + * @param parentAccountId + * @param targetAccountId + * @param primaryAdminUserId + * @param companyName + * @returns partner info + */ + async updatePartnerInfo( + context: Context, + parentAccountId: number, + targetAccountId: number, + primaryAdminUserId: number, + companyName: string, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const accountRepo = entityManager.getRepository(Account); + const userRepo = entityManager.getRepository(User); + + // 指定したプライマリ管理者が対象アカウント内に存在するかチェック + const primaryAdminUser = await userRepo.findOne({ + where: { + id: primaryAdminUserId, + account_id: targetAccountId, + email_verified: true, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!primaryAdminUser) { + throw new AdminUserNotFoundError( + `Primary admin user is not found. id: ${primaryAdminUserId}, account_id: ${targetAccountId}`, + ); + } + + // 対象アカウントが存在するかチェック + const targetAccount = await accountRepo.findOne({ + where: { id: targetAccountId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!targetAccount) { + throw new AccountNotFoundError( + `Account is not found. id: ${targetAccountId}`, + ); + } + + // 実行者のアカウントが対象アカウントの親アカウントであるか確認する + if (parentAccountId !== targetAccount.parent_account_id) { + throw new HierarchyMismatchError( + `Target account is not child account. parentAccountId: ${parentAccountId}, targetAccountId: ${targetAccountId}`, + ); + } + + // accountsテーブルレコード更新 + await updateEntity( + accountRepo, + { id: targetAccountId }, + { + company_name: companyName, + primary_admin_user_id: primaryAdminUserId, + }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/accounts/entity/account.entity.ts b/dictation_server/src/repositories/accounts/entity/account.entity.ts index 3c40a03..255c9c1 100644 --- a/dictation_server/src/repositories/accounts/entity/account.entity.ts +++ b/dictation_server/src/repositories/accounts/entity/account.entity.ts @@ -1,4 +1,5 @@ import { bigintTransformer } from '../../../common/entity'; +import { FILE_RETENTION_DAYS_DEFAULT } from '../../../constants'; import { User } from '../../../repositories/users/entity/user.entity'; import { Entity, @@ -44,6 +45,12 @@ export class Account { @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) active_worktype_id: number | null; + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: FILE_RETENTION_DAYS_DEFAULT }) + file_retention_days: number; + @Column({ nullable: true, type: 'datetime' }) deleted_at: Date | null; diff --git a/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts new file mode 100644 index 0000000..d78c0ba --- /dev/null +++ b/dictation_server/src/repositories/accounts/entity/account_archive.entity.ts @@ -0,0 +1,66 @@ +import { bigintTransformer } from '../../../common/entity'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity({ name: 'accounts_archive' }) +export class AccountArchive { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + parent_account_id: number | null; + + @Column() + tier: number; + + @Column() + country: string; + + @Column({ default: false }) + delegation_permission: boolean; + + @Column({ default: false }) + locked: boolean; + + @Column({ default: false }) + verified: boolean; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + primary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + secondary_admin_user_id: number | null; + + @Column({ nullable: true, type: 'bigint', transformer: bigintTransformer }) + active_worktype_id: number | null; + + @Column({ default: false }) + auto_file_delete: boolean; + + @Column({ default: 0 }) + file_retention_days: number; + + @Column({ nullable: true, type: 'datetime' }) + deleted_at: Date | null; + + @Column({ nullable: true, type: 'datetime' }) + created_by: string | null; + + @CreateDateColumn({ + type: 'datetime', + }) + created_at: Date; + + @Column({ nullable: true, type: 'datetime' }) + updated_by: string | null; + + @UpdateDateColumn({ + type: 'datetime', + }) + updated_at: Date; +} diff --git a/dictation_server/src/repositories/accounts/errors/types.ts b/dictation_server/src/repositories/accounts/errors/types.ts index 826700c..2c661e3 100644 --- a/dictation_server/src/repositories/accounts/errors/types.ts +++ b/dictation_server/src/repositories/accounts/errors/types.ts @@ -26,3 +26,29 @@ export class AccountLockedError extends Error { this.name = 'AccountLockedError'; } } + +// パートナーアカウント削除不可エラー +export class PartnerAccountDeletionError extends Error { + constructor(message: string) { + super(message); + this.name = 'PartnerAccountDeletionError'; + } +} +/** + * 階層構造関係不適切エラー + */ +export class HierarchyMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'HierarchyMismatchError'; + } +} +/** + * 所属リージョン不一致エラー + */ +export class RegionMismatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'RegionMismatchError'; + } +} diff --git a/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts index b9605e8..a6dded1 100644 --- a/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts +++ b/dictation_server/src/repositories/audio_files/audio_files.repository.service.ts @@ -1,7 +1,182 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In, Not } from 'typeorm'; +import { AudioFile } from './entity/audio_file.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { User } from '../users/entity/user.entity'; +import { USER_ROLES } from '../../constants'; +import { UserGroupMember } from '../user_groups/entity/user_group_member.entity'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; +import { Context } from '../../common/log'; +import { updateEntity } from '../../common/repository'; +import { + CheckoutPermissionNotFoundError, + FileNameAlreadyExistsError, + RoleNotMatchError, + TasksNotFoundError, +} from './errors/types'; +import { AuthorUserNotMatchError } from '../../features/files/errors/types'; @Injectable() export class AudioFilesRepositoryService { + //クエリログにコメントを出力するかどうか + private readonly isCommentOut = process.env.STAGE !== 'local'; constructor(private dataSource: DataSource) {} + + /** + * アカウント内のテンプレートファイルの一覧を取得する + * @param context + * @param accountId + * @param userId + * @param audioFileId + * @param fileName + * @returns audio file + */ + async renameAudioFile( + context: Context, + accountId: number, + userId: number, + audioFileId: number, + fileName: string, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + // 実行ユーザーの情報を取得 + const userRepo = entityManager.getRepository(User); + const user = await userRepo.findOne({ + where: { account_id: accountId, id: userId }, + relations: { account: true }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!user) { + throw new Error( + `user not found. account_id: ${accountId}, user_id: ${userId}`, + ); + } + const account = user.account; + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!account) { + throw new Error(`account not found. account_id: ${accountId}`); + } + + // ユーザーがアカウントの管理者であるかどうか + const isAdmin = + account.primary_admin_user_id === userId || + account.secondary_admin_user_id === userId; + + // ユーザーがTypistである場合は、ユーザーの所属するグループを取得しておく + let groupIds: number[] = []; + if (user.role === USER_ROLES.TYPIST) { + const groupMemberRepo = entityManager.getRepository(UserGroupMember); + // ユーザーの所属するすべてのグループを列挙 + const groups = await groupMemberRepo.find({ + relations: { user: true }, + where: { user_id: userId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + // ユーザーの所属するすべてのグループIDを列挙 + groupIds = groups.map((member) => member.user_group_id); + } + + // リクエストの音声ファイル名と同じファイル名の音声ファイルの一覧を取得 + const audioFileRepo = entityManager.getRepository(AudioFile); + const audioFiles = await audioFileRepo.find({ + where: { + account_id: accountId, + id: Not(audioFileId), + file_name: fileName, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + // ファイル名が重複している場合はエラー + if (audioFiles.length !== 0) { + throw new FileNameAlreadyExistsError( + `The file name already exists. accountId: ${accountId} file_name: ${fileName}`, + ); + } + + // タスク情報を取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + where: { account_id: accountId, audio_file_id: audioFileId }, + relations: { file: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + lock: { mode: 'pessimistic_write' }, + }); + + if (!task) { + throw new TasksNotFoundError( + `task not found. account_id: ${accountId} audio_file_id: ${audioFileId}`, + ); + } + + // 音声ファイル情報を取得 + const audioFile = task.file; + if (!audioFile) { + throw new TasksNotFoundError( + `audioFile not found. audio_file_id: ${audioFileId}`, + ); + } + + if (isAdmin) { + // 管理者の場合は、ファイル名を変更できる + await updateEntity( + audioFileRepo, + { id: audioFileId }, + { file_name: fileName }, + this.isCommentOut, + context, + ); + return; + } + + // ユーザーが管理者でない場合は、ロールに応じた権限を確認 + + if (user.role === USER_ROLES.NONE) { + // NONEの場合はエラー + throw new RoleNotMatchError( + `The user does not have the required role. userId: ${userId}. role: ${user.role}`, + ); + } + if (user.role === USER_ROLES.AUTHOR) { + // ユーザーがAuthorである場合は、音声ファイルのAuthorIDが一致するか確認 + if (audioFile.author_id !== user.author_id) { + throw new AuthorUserNotMatchError( + `The user is not the author of the audio file. audioFileId: ${audioFileId}, userId: ${userId}`, + ); + } + } + if (user.role === USER_ROLES.TYPIST) { + // ユーザーがTypistである場合は、チェックアウト権限を確認 + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + // ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得 + const checkoutPermissions = await checkoutRepo.find({ + where: [ + { task_id: task.id, user_id: userId }, // ユーザーがチェックアウト可能である + { task_id: task.id, user_group_id: In(groupIds) }, // ユーザーの所属するユーザーグループがチェックアウト可能である + ], + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // チェックアウト権限がない場合はエラー + if (checkoutPermissions.length === 0) { + throw new CheckoutPermissionNotFoundError( + `The user does not have checkout permission. taskId: ${task.id}, userId: ${userId}`, + ); + } + } + + // ファイル名を変更 + await updateEntity( + audioFileRepo, + { id: audioFileId }, + { file_name: fileName }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts b/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts index 5a0d9f5..8332760 100644 --- a/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts +++ b/dictation_server/src/repositories/audio_files/entity/audio_file.entity.ts @@ -15,6 +15,8 @@ export class AudioFile { @Column() file_name: string; @Column() + raw_file_name: string; + @Column() author_id: string; @Column() work_type_id: string; diff --git a/dictation_server/src/repositories/audio_files/errors/types.ts b/dictation_server/src/repositories/audio_files/errors/types.ts new file mode 100644 index 0000000..9d47793 --- /dev/null +++ b/dictation_server/src/repositories/audio_files/errors/types.ts @@ -0,0 +1,35 @@ +// タスク未発見エラー +export class TasksNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'TasksNotFoundError'; + } +} +// ファイル名変更権限ロール不一致エラー +export class RoleNotMatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'RoleNotMatchError'; + } +} +// タスクAuthorID不一致エラー +export class TaskAuthorIdNotMatchError extends Error { + constructor(message: string) { + super(message); + this.name = 'TaskAuthorIdNotMatchError'; + } +} +// チェックアウト権限未発見エラー +export class CheckoutPermissionNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = 'CheckoutPermissionNotFoundError'; + } +} +// 同名ファイルエラー +export class FileNameAlreadyExistsError extends Error { + constructor(message: string) { + super(message); + this.name = 'StatusNotMatchError'; + } +} diff --git a/dictation_server/src/repositories/job_number/entity/job_number.entity.ts b/dictation_server/src/repositories/job_number/entity/job_number.entity.ts new file mode 100644 index 0000000..ec2373e --- /dev/null +++ b/dictation_server/src/repositories/job_number/entity/job_number.entity.ts @@ -0,0 +1,25 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { bigintTransformer } from '../../../common/entity'; + +@Entity({ name: 'job_number' }) +export class JobNumber { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'bigint', transformer: bigintTransformer }) + account_id: number; + + @Column() + job_number: string; + + @UpdateDateColumn({ + default: () => "datetime('now', 'localtime')", + type: 'datetime', + }) // defaultはSQLite用設定値.本番用は別途migrationで設定 + updated_at: Date; +} diff --git a/dictation_server/src/repositories/job_number/job_number.repository.module.ts b/dictation_server/src/repositories/job_number/job_number.repository.module.ts new file mode 100644 index 0000000..a39f9bd --- /dev/null +++ b/dictation_server/src/repositories/job_number/job_number.repository.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { JobNumber } from './entity/job_number.entity'; +import { JobNumberRepositoryService } from './job_number.repository.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([JobNumber])], + providers: [JobNumberRepositoryService], + exports: [JobNumberRepositoryService], +}) +export class JobNumberRepositoryModule {} diff --git a/dictation_server/src/repositories/job_number/job_number.repository.service.ts b/dictation_server/src/repositories/job_number/job_number.repository.service.ts new file mode 100644 index 0000000..5790f00 --- /dev/null +++ b/dictation_server/src/repositories/job_number/job_number.repository.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class JobNumberRepositoryService { + constructor(private dataSource: DataSource) {} +} diff --git a/dictation_server/src/repositories/licenses/licenses.repository.service.ts b/dictation_server/src/repositories/licenses/licenses.repository.service.ts index 6270b43..5b6e873 100644 --- a/dictation_server/src/repositories/licenses/licenses.repository.service.ts +++ b/dictation_server/src/repositories/licenses/licenses.repository.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import { DataSource, In } from 'typeorm'; +import { DataSource, In, IsNull, MoreThanOrEqual, Not } from 'typeorm'; import { LicenseOrder, License, @@ -12,6 +12,7 @@ import { LICENSE_ALLOCATED_STATUS, LICENSE_ISSUE_STATUS, LICENSE_TYPE, + STORAGE_SIZE_PER_LICENSE, SWITCH_FROM_TYPE, TIERS, USER_LICENSE_STATUS, @@ -41,6 +42,7 @@ import { import { Context } from '../../common/log'; import { User } from '../users/entity/user.entity'; import { UserNotFoundError } from '../users/errors/types'; +import { AudioFile } from '../audio_files/entity/audio_file.entity'; @Injectable() export class LicensesRepositoryService { @@ -523,9 +525,14 @@ export class LicensesRepositoryService { context: Context, myAccountId: number, ): Promise { - const nowDate = new DateWithZeroTime(); const licenseRepo = this.dataSource.getRepository(License); // EntityManagerではorderBy句で、expiry_dateに対して複数条件でソートを使用するため出来ない為、createQueryBuilderを使用する。 + // プロダクト バックログ項目 4218: [FB対応]有効期限当日のライセンスは一覧に表示しない の対応 + // 有効期限が当日のライセンスは取得しない + // 明日の00:00:00を取得 + const tomorrowDate = new DateWithZeroTime( + new Date().setDate(new Date().getDate() + 1), + ); const queryBuilder = licenseRepo .createQueryBuilder('license') .where('license.account_id = :accountId', { accountId: myAccountId }) @@ -536,8 +543,8 @@ export class LicensesRepositoryService { ], }) .andWhere( - '(license.expiry_date >= :nowDate OR license.expiry_date IS NULL)', - { nowDate }, + '(license.expiry_date >= :tomorrowDate OR license.expiry_date IS NULL)', + { tomorrowDate }, ) .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`) .orderBy('license.expiry_date IS NULL', 'DESC') @@ -595,6 +602,9 @@ export class LicensesRepositoryService { } // 期限切れの場合はエラー + // 有効期限が当日のライセンスは割り当て不可 + // XXX 記述は「有効期限が過去のライセンスは割り当て不可」のような意図だと思われるが、実際の処理は「有効期限が当日のライセンスは割り当て不可」になっている + // より正確な記述に修正したほうが良いが、リリース後のため、修正は保留(2024年6月7日) if (targetLicense.expiry_date) { const currentDay = new Date(); currentDay.setHours(23, 59, 59, 999); @@ -862,4 +872,53 @@ export class LicensesRepositoryService { return { state: USER_LICENSE_STATUS.ALLOCATED }; } + /** + * ストレージ情報(上限と使用量)を取得します + * @param context + * @param accountId + * @param currentDate + * @returns size: ストレージ上限, used: 使用量 + */ + async getStorageInfo( + context: Context, + accountId: number, + currentDate: Date, + ): Promise<{ size: number; used: number }> { + return await this.dataSource.transaction(async (entityManager) => { + // ストレージ上限計算のための値を取得する。(ユーザーに一度でも割り当てたことのあるライセンス数) + const licenseRepo = entityManager.getRepository(License); + const licensesAllocatedOnce = await licenseRepo.count({ + where: { + account_id: accountId, + expiry_date: MoreThanOrEqual(currentDate), + status: In([ + LICENSE_ALLOCATED_STATUS.ALLOCATED, + LICENSE_ALLOCATED_STATUS.REUSABLE, + ]), + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ストレージ上限を計算する + const size = + licensesAllocatedOnce * STORAGE_SIZE_PER_LICENSE * 1000 * 1000 * 1000; // GB -> B + + // 既に使用しているストレージ量を取得する + const audioFileRepo = entityManager.getRepository(AudioFile); + const usedQuery = await audioFileRepo + .createQueryBuilder('audioFile') + .select('SUM(audioFile.file_size)', 'used') + .where('audioFile.account_id = :accountId', { accountId }) + .comment(`${context.getTrackingId()}_${new Date().toUTCString()}`) + .getRawOne(); + + let used = parseInt(usedQuery?.used); + if (isNaN(used)) { + // AudioFileのレコードが存在しない場合、SUM関数がNULLを返すため、0を返す + used = 0; + } + + return { size, used }; + }); + } } diff --git a/dictation_server/src/repositories/tasks/tasks.repository.service.ts b/dictation_server/src/repositories/tasks/tasks.repository.service.ts index 66202f2..e43c2cf 100644 --- a/dictation_server/src/repositories/tasks/tasks.repository.service.ts +++ b/dictation_server/src/repositories/tasks/tasks.repository.service.ts @@ -10,7 +10,13 @@ import { Repository, } from 'typeorm'; import { Task } from './entity/task.entity'; -import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants'; +import { + ADMIN_ROLES, + MAX_JOB_NUMBER, + NODE_ENV_TEST, + TASK_STATUS, + USER_ROLES, +} from '../../constants'; import { AudioOptionItem as ParamOptionItem } from '../../features/files/types/types'; import { AudioFile } from '../audio_files/entity/audio_file.entity'; import { AudioOptionItem } from '../audio_option_items/entity/audio_option_item.entity'; @@ -49,6 +55,7 @@ import { } from '../../common/repository'; import { Context } from '../../common/log'; import { UserNotFoundError } from '../users/errors/types'; +import { JobNumber } from '../job_number/entity/job_number.entity'; @Injectable() export class TasksRepositoryService { @@ -848,7 +855,8 @@ export class TasksRepositoryService { audioFile.account_id = account_id; audioFile.owner_user_id = owner_user_id; audioFile.url = url; - audioFile.file_name = file_name; + audioFile.file_name = file_name.replace('.zip', ''); + audioFile.raw_file_name = file_name; audioFile.author_id = author_id; audioFile.work_type_id = work_type_id; audioFile.started_at = started_at; @@ -899,25 +907,29 @@ export class TasksRepositoryService { const taskRepo = entityManager.getRepository(Task); - // バグ 3954: [4/8リリース]タスクをすべてBackupした後、タスクを作成するとジョブナンバーが1から採番される(暫定対応) - // アカウント内で最新タスクのタスクを取得し、そのJOBナンバーをインクリメントして新しいタスクのJOBナンバーを設定する - const lastTask = await taskRepo.findOne({ + // バグ 3954: [4/8リリース]タスクをすべてBackupした後、タスクを作成するとジョブナンバーが1から採番される(恒久対応) + // job_numberテーブルから最新のJOBナンバーを取得 + const jobNumberRepo = entityManager.getRepository(JobNumber); + const currentJobNumberData = await jobNumberRepo.findOne({ where: { account_id: account_id }, - order: { created_at: 'DESC', job_number: 'DESC' }, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, lock: { mode: 'pessimistic_write' }, }); + // JOBナンバーが存在しない場合はエラー + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!currentJobNumberData) { + throw new Error(`JobNumber not exists. account_id:${account_id}`); + } - let newJobNumber = '00000001'; - if (!lastTask) { - // 初回は00000001 - newJobNumber = '00000001'; - } else if (lastTask.job_number === '99999999') { + let newJobNumber: string = ''; + if (currentJobNumberData.job_number === MAX_JOB_NUMBER) { // 末尾なら00000001に戻る newJobNumber = '00000001'; } else { // 最新のJOBナンバーをインクリメントして次の番号とする - newJobNumber = `${Number(lastTask.job_number) + 1}`.padStart(8, '0'); + newJobNumber = `${ + Number(currentJobNumberData.job_number) + 1 + }`.padStart(8, '0'); } task.job_number = newJobNumber; @@ -929,6 +941,15 @@ export class TasksRepositoryService { context, ); + // JobNumberを更新 + await updateEntity( + jobNumberRepo, + { account_id: account_id }, + { job_number: newJobNumber }, + this.isCommentOut, + context, + ); + const optionItems = paramOptionItems.map((x) => { return { audio_file_id: persisted.audio_file_id, @@ -1340,6 +1361,123 @@ export class TasksRepositoryService { ); }); } + /** + * Deletes task + * @param context + * @param userId 削除を行うユーザーID + * @param audioFileId 削除を行うタスクの音声ファイルID + * @returns task + */ + async deleteTask( + context: Context, + userId: number, + audioFileId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + + // 削除を行うユーザーとアカウントを取得 + const user = await userRepo.findOne({ + where: { id: userId }, + relations: { account: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!user) { + throw new Error(`user not found. userId:${userId}`); + } + const account = user.account; + if (!account) { + throw new Error(`account not found. userId:${userId}`); + } + + // ユーザーがアカウントの管理者であるかを確認 + const isAdmin = + account.primary_admin_user_id === userId || + account.secondary_admin_user_id === userId; + + // 削除を行うタスクを取得 + const taskRepo = entityManager.getRepository(Task); + const task = await taskRepo.findOne({ + where: { + account_id: account.id, + audio_file_id: audioFileId, + }, + relations: { file: true }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + // テスト環境の場合はロックを行わない(sqliteがlockに対応していないため) + lock: + process.env.NODE_ENV !== NODE_ENV_TEST + ? { mode: 'pessimistic_write' } + : undefined, + }); + if (!task) { + throw new TasksNotFoundError( + `task not found. audio_file_id:${audioFileId}`, + ); + } + if (!task.file) { + throw new Error(`audio file not found. audio_file_id:${audioFileId}`); + } + + // ユーザーが管理者でない場合は、タスクのAuthorIdとユーザーのAuthorIdが一致するかを確認 + if (!isAdmin) { + // ユーザーがAuthorである場合 + if (user.role === USER_ROLES.AUTHOR) { + if (task.file.author_id !== user.author_id) { + throw new TaskAuthorIdNotMatchError( + `Task authorId not match. userId:${userId}, authorId:${user.author_id}`, + ); + } + } else { + // ユーザーが管理者でもAuthorでもない場合はエラー + throw new Error(`The user is not admin or author. userId:${userId}`); + } + } + + // タスクのステータスがInProgress・Pendingの時はエラー + if ( + task.status === TASK_STATUS.IN_PROGRESS || + task.status === TASK_STATUS.PENDING + ) { + throw new StatusNotMatchError( + `task status is InProgress or Pending. status:${task.status} , audio_file_id:${audioFileId}`, + ); + } + // タスクに紐づくオプションアイテムを削除 + const optionItemRepo = entityManager.getRepository(AudioOptionItem); + await deleteEntity( + optionItemRepo, + { audio_file_id: task.audio_file_id }, + this.isCommentOut, + context, + ); + // タスクに紐づくチェックアウト候補を削除 + const checkoutRepo = entityManager.getRepository(CheckoutPermission); + await deleteEntity( + checkoutRepo, + { task_id: task.id }, + this.isCommentOut, + context, + ); + // タスクを削除 + await deleteEntity( + taskRepo, + { audio_file_id: audioFileId }, + this.isCommentOut, + context, + ); + // タスクに紐づく音声ファイル情報を削除 + const audioFileRepo = entityManager.getRepository(AudioFile); + await deleteEntity( + audioFileRepo, + { id: audioFileId }, + this.isCommentOut, + context, + ); + }); + } /** * workflowに紐づけられているタイピスト・タイピストグループで、タスクのチェックアウト権限を設定 diff --git a/dictation_server/src/repositories/template_files/errors/types.ts b/dictation_server/src/repositories/template_files/errors/types.ts index d1db4d7..9cff27f 100644 --- a/dictation_server/src/repositories/template_files/errors/types.ts +++ b/dictation_server/src/repositories/template_files/errors/types.ts @@ -5,3 +5,17 @@ export class TemplateFileNotExistError extends Error { this.name = 'TemplateFileNotExistError'; } } + +export class WorkflowHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'WorkflowHasTemplateDeleteFailedError'; + } +} + +export class NotFinishedTaskHasTemplateDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'NotFinishedTaskHasTemplateDeleteFailedError'; + } +} diff --git a/dictation_server/src/repositories/template_files/template_files.repository.service.ts b/dictation_server/src/repositories/template_files/template_files.repository.service.ts index 5fc805b..c1ec96c 100644 --- a/dictation_server/src/repositories/template_files/template_files.repository.service.ts +++ b/dictation_server/src/repositories/template_files/template_files.repository.service.ts @@ -1,8 +1,20 @@ import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { DataSource, In } from 'typeorm'; import { TemplateFile } from './entity/template_file.entity'; -import { insertEntity, updateEntity } from '../../common/repository'; +import { + deleteEntity, + insertEntity, + updateEntity, +} from '../../common/repository'; import { Context } from '../../common/log'; +import { + NotFinishedTaskHasTemplateDeleteFailedError, + TemplateFileNotExistError, + WorkflowHasTemplateDeleteFailedError, +} from './errors/types'; +import { Workflow } from '../workflows/entity/workflow.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { TASK_STATUS } from '../../constants'; @Injectable() export class TemplateFilesRepositoryService { @@ -32,6 +44,36 @@ export class TemplateFilesRepositoryService { }); } + /** + * アカウント内のIDで指定されたテンプレートファイルを取得する + * @param context + * @param accountId + * @param templateFileId + * @returns template file + */ + async getTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + return await this.dataSource.transaction(async (entityManager) => { + const templateFilesRepo = entityManager.getRepository(TemplateFile); + + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + return template; + }); + } + /** * アカウント内にテンプレートファイルを追加(すでに同名ファイルがあれば更新)する * @param accountId @@ -79,4 +121,92 @@ export class TemplateFilesRepositoryService { } }); } + + /** + * アカウント内にある指定されたテンプレートファイルを削除する + * @param accountId + * @param fileName + * @param url + * @returns template file + */ + async deleteTemplateFile( + context: Context, + accountId: number, + templateFileId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const workflowRepo = entityManager.getRepository(Workflow); + // テンプレートファイルがワークフローで使用されているか確認 + const workflow = await workflowRepo.findOne({ + where: { + account_id: accountId, + template_id: templateFileId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ワークフローで使用されている場合はエラー + if (workflow) { + throw new WorkflowHasTemplateDeleteFailedError( + `workflow has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const templateFilesRepo = entityManager.getRepository(TemplateFile); + // アカウント内に指定IDファイルがあるか確認 + const template = await templateFilesRepo.findOne({ + where: { account_id: accountId, id: templateFileId }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ファイルが存在しない場合はエラー + if (!template) { + throw new TemplateFileNotExistError( + `template file not found. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + const taskRepo = entityManager.getRepository(Task); + // テンプレートファイルが未完了タスクで使用されているか確認 + const templateUsedTasks = await taskRepo.findOne({ + where: { + account_id: accountId, + template_file_id: templateFileId, + status: In([ + TASK_STATUS.UPLOADED, + TASK_STATUS.IN_PROGRESS, + TASK_STATUS.PENDING, + ]), + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 未完了のタスクでテンプレートファイルが使用されている場合はエラー + if (templateUsedTasks) { + throw new NotFinishedTaskHasTemplateDeleteFailedError( + `not finished task has template file. accountId: ${accountId}, templateFileId: ${templateFileId}`, + ); + } + + // テンプレートファイルの削除 + await deleteEntity( + templateFilesRepo, + { id: templateFileId }, + this.isCommentOut, + context, + ); + + // 完了済みのタスクからテンプレートファイルの紐づけを解除 + await updateEntity( + taskRepo, + { template_file_id: templateFileId }, + { template_file_id: null }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/user_groups/errors/types.ts b/dictation_server/src/repositories/user_groups/errors/types.ts index a499a27..a1ebabe 100644 --- a/dictation_server/src/repositories/user_groups/errors/types.ts +++ b/dictation_server/src/repositories/user_groups/errors/types.ts @@ -12,6 +12,22 @@ export class TypistIdInvalidError extends Error { this.name = 'TypistIdInvalidError'; } } + +// 削除対象グループがWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowDeleteFailedError'; + } +} + +// 削除対象グループがチェックアウト権限を持っている事が原因の削除失敗エラー +export class ExistsCheckoutPermissionDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsCheckoutPermissionDeleteFailedError'; + } +} // 同名のタイピストグループが存在する場合のエラー export class TypistGroupNameAlreadyExistError extends Error { constructor(message: string) { diff --git a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts index a900569..8fbbc25 100644 --- a/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts +++ b/dictation_server/src/repositories/user_groups/user_groups.repository.service.ts @@ -3,7 +3,10 @@ import { DataSource, In, IsNull, Not } from 'typeorm'; import { UserGroup } from './entity/user_group.entity'; import { UserGroupMember } from './entity/user_group_member.entity'; import { User } from '../users/entity/user.entity'; +import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity'; import { + AssignedWorkflowDeleteFailedError, + ExistsCheckoutPermissionDeleteFailedError, TypistGroupNameAlreadyExistError, TypistGroupNotExistError, TypistIdInvalidError, @@ -16,6 +19,7 @@ import { deleteEntity, } from '../../common/repository'; import { Context } from '../../common/log'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; @Injectable() export class UserGroupsRepositoryService { @@ -285,4 +289,81 @@ export class UserGroupsRepositoryService { return typistGroup; }); } + + /** + * 指定したIDのタイピストグループを削除します + * @param context + * @param accountId + * @param typistGroupId + * @returns typist group + */ + async deleteTypistGroup( + context: Context, + accountId: number, + typistGroupId: number, + ): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userGroupRepo = entityManager.getRepository(UserGroup); + // GroupIdが自アカウント内に存在するか確認する + const typistGroup = await userGroupRepo.findOne({ + relations: { userGroupMembers: true }, + where: { + id: typistGroupId, + account_id: accountId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + if (!typistGroup) { + throw new TypistGroupNotExistError( + `TypistGroup not exists Error. accountId: ${accountId}; typistGroupId: ${typistGroupId}`, + ); + } + + // ルーティングルールに紐づくタイピストグループは削除できない + const workflowTypistRepo = entityManager.getRepository(WorkflowTypist); + const workflowTypist = await workflowTypistRepo.findOne({ + where: { typist_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (workflowTypist) { + throw new AssignedWorkflowDeleteFailedError( + `Typist Group is used in routing rule. typistGroupId: ${typistGroupId}`, + ); + } + + // タスクのチェックアウト候補のタイピストグループは削除できない + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + + const checkoutPermission = await checkoutPermissionRepo.findOne({ + where: { user_group_id: typistGroupId }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + if (checkoutPermission) { + throw new ExistsCheckoutPermissionDeleteFailedError( + `Typist Group is used in task checkout permission. typistGroupId: ${typistGroupId}`, + ); + } + + const userGroupMemberRepo = entityManager.getRepository(UserGroupMember); + // 対象のタイピストグループのユーザーを削除する + await deleteEntity( + userGroupMemberRepo, + { user_group_id: typistGroupId }, + this.isCommentOut, + context, + ); + + // 対象のタイピストグループを削除する + await deleteEntity( + userGroupRepo, + { id: typistGroupId }, + this.isCommentOut, + context, + ); + }); + } } diff --git a/dictation_server/src/repositories/users/errors/types.ts b/dictation_server/src/repositories/users/errors/types.ts index 6f90ede..c5352fa 100644 --- a/dictation_server/src/repositories/users/errors/types.ts +++ b/dictation_server/src/repositories/users/errors/types.ts @@ -54,3 +54,59 @@ export class DelegationNotAllowedError extends Error { this.name = 'DelegationNotAllowedError'; } } + +// 削除対象ユーザーが管理者である事が原因の削除失敗エラー +export class AdminDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AdminDeleteFailedError'; + } +} + +// 削除対象ユーザー(Author)がWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowWithAuthorDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowWithAuthorDeleteFailedError'; + } +} + +// 削除対象ユーザー(Typist)がWorkflowにアサインされている事が原因の削除失敗エラー +export class AssignedWorkflowWithTypistDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'AssignedWorkflowWithTypistDeleteFailedError'; + } +} + +// 削除対象ユーザーがGroupに所属している事が原因の削除失敗エラー +export class ExistsGroupMemberDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsGroupMemberDeleteFailedError'; + } +} + +// 削除対象ユーザー(Author)が作成したタスクがまだ残っている事が原因の削除失敗エラー +export class ExistsTaskDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsTaskDeleteFailedError'; + } +} + +// 削除対象ユーザーがチェックアウト権限を持っているor文字起こし担当者としてアサインされている事が原因の削除失敗エラー +export class ExistsValidCheckoutDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsValidCheckoutDeleteFailedError'; + } +} + +// 削除対象ユーザーが有効なライセンスをまだ持っている事が原因の削除失敗エラー +export class ExistsValidLicenseDeleteFailedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ExistsValidLicenseDeleteFailedError'; + } +} diff --git a/dictation_server/src/repositories/users/users.repository.service.ts b/dictation_server/src/repositories/users/users.repository.service.ts index 9a70a96..9e4bc39 100644 --- a/dictation_server/src/repositories/users/users.repository.service.ts +++ b/dictation_server/src/repositories/users/users.repository.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { User, newUser } from './entity/user.entity'; +import { User, UserArchive, newUser } from './entity/user.entity'; import { DataSource, FindOptionsWhere, @@ -21,17 +21,29 @@ import { TermInfoNotFoundError, UpdateTermsVersionNotSetError, DelegationNotAllowedError, + ExistsGroupMemberDeleteFailedError, + AssignedWorkflowWithTypistDeleteFailedError, + AssignedWorkflowWithAuthorDeleteFailedError, + AdminDeleteFailedError, + ExistsValidLicenseDeleteFailedError, + ExistsValidCheckoutDeleteFailedError, + ExistsTaskDeleteFailedError, } from './errors/types'; import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE, + SWITCH_FROM_TYPE, + TASK_STATUS, TERM_TYPE, TIERS, TRIAL_LICENSE_ISSUE_NUM, USER_ROLES, USER_ROLE_ORDERS, } from '../../constants'; -import { License } from '../licenses/entity/license.entity'; +import { + License, + LicenseAllocationHistory, +} from '../licenses/entity/license.entity'; import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types'; import { Term } from '../terms/entity/term.entity'; import { TermsCheckInfo } from '../../features/auth/types/types'; @@ -49,6 +61,10 @@ import { updateEntity, deleteEntity, } from '../../common/repository'; +import { UserGroup } from '../user_groups/entity/user_group.entity'; +import { Task } from '../tasks/entity/task.entity'; +import { CheckoutPermission } from '../checkout_permissions/entity/checkout_permission.entity'; +import { WorkflowTypist } from '../workflows/entity/workflow_typists.entity'; @Injectable() export class UsersRepositoryService { @@ -173,7 +189,26 @@ export class UsersRepositoryService { } return user; } + /** + * アカウントIDをもとにユーザー一覧を取得します。 + * @param context + * @param accountId 検索対象のアカウントID + * @returns users[] + */ + async findUsersByAccountId( + context: Context, + accountId: number, + ): Promise { + const users = await this.dataSource.getRepository(User).find({ + where: { + email_verified: true, + account_id: accountId, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + return users; + } /** * AuthorIDをもとにユーザーを取得します。 * AuthorIDがセットされていない場合や、ユーザーが存在しない場合はエラーを返します。 @@ -363,7 +398,7 @@ export class UsersRepositoryService { } /** - * 管理ユーザーがメール認証済みなら認証情報を更新する + * ユーザーがメール認証済みなら認証情報を更新する * @param user * @returns update */ @@ -401,6 +436,40 @@ export class UsersRepositoryService { }); } + /** + * ユーザーをメール未認証にする + * @param user + * @param context + * @param id + * @returns void + */ + async updateUserUnverified(context: Context, id: number): Promise { + await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + const targetUser = await userRepo.findOne({ + where: { + id: id, + }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理 + if (!targetUser) { + throw new UserNotFoundError(`User not Found.`); + } + + targetUser.email_verified = false; + + await updateEntity( + userRepo, + { id: targetUser.id }, + targetUser, + this.isCommentOut, + context, + ); + }); + } + /** * Emailを認証済みにして、トライアルライセンスを作成する * @param id @@ -574,6 +643,251 @@ export class UsersRepositoryService { }); } + /** + * Deletes user + * @param context Context + * @param userId 削除対象ユーザーのID + * @param currentTime ライセンス有効期限のチェックに使用する現在時刻 + * @returns user + */ + async deleteUser( + context: Context, + userId: number, + currentTime: Date, + ): Promise<{ isSuccess: boolean }> { + return await this.dataSource.transaction(async (entityManager) => { + const userRepo = entityManager.getRepository(User); + // 削除対象ユーザーをロックを取った上で取得 + const target = await userRepo.findOne({ + relations: { + account: true, + }, + where: { + id: userId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + // 削除済みであれば失敗する + if (target == null) { + return { isSuccess: false }; + } + + const { account } = target; + if (account == null) { + // 通常ありえないが、アカウントが存在しない場合はエラー + throw new AccountNotFoundError('Account is Not Found.'); + } + + // 管理者IDの一覧を作成 + const adminIds = [account] + .flatMap((x) => [x?.primary_admin_user_id, x?.secondary_admin_user_id]) + .flatMap((x) => (x != null ? [x] : [])); + // 削除対象が管理者であるかを確認 + if (adminIds.some((adminId) => adminId === target.id)) { + throw new AdminDeleteFailedError('User is an admin.'); + } + const userGroupRepo = entityManager.getRepository(UserGroup); + const groups = await userGroupRepo.find({ + relations: { + userGroupMembers: true, + }, + where: { + account_id: target.account_id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + const workflowRepo = entityManager.getRepository(Workflow); + const workflows = await workflowRepo.find({ + where: { + account_id: target.account_id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // WorkflowのAuthor枠に設定されているユーザーは削除できない + if (workflows.some((x) => x.author_id === target.id)) { + throw new AssignedWorkflowWithAuthorDeleteFailedError( + 'Author is assigned to a workflow.', + ); + } + const workflowTypistsRepo = entityManager.getRepository(WorkflowTypist); + const workflowTypists = await workflowTypistsRepo.find({ + where: { + typist_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + // Workflowに直接個人で指定されているTypistは削除できない + if (workflowTypists.some((x) => x.typist_id === target.id)) { + throw new AssignedWorkflowWithTypistDeleteFailedError( + 'Typist is assigned to a workflow.', + ); + } + // いずれかのGroupに属しているユーザーIDの一覧を作成 + const groupMemberIds = groups + .flatMap((group) => group.userGroupMembers) + .flatMap((member) => (member != null ? [member.user_id] : [])); + + // 削除対象ユーザーがGroupに属しているユーザーに含まれていたら削除できない + if (groupMemberIds.some((id) => id === target.id)) { + throw new ExistsGroupMemberDeleteFailedError( + 'User is a member of a group', + ); + } + // 削除対象ユーザーがAuthorであった時、 + if (target.role === USER_ROLES.AUTHOR) { + const taskRepo = entityManager.getRepository(Task); + // 自分が所有者のタスクが存在するか確認 + const ownerTasksExist = await taskRepo.exist({ + relations: { + file: true, + }, + where: [ + { + account_id: target.account_id, + file: { + owner_user_id: target.id, + }, + }, + { + account_id: target.account_id, + file: { + author_id: target.author_id ?? undefined, + }, + }, + ], + lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + // 削除対象のユーザーが所有者のタスクが存在する場合は削除できない + if (ownerTasksExist) { + throw new ExistsTaskDeleteFailedError('User has tasks.'); + } + } + // 削除対象ユーザーがTypistであった時、 + if (target.role === USER_ROLES.TYPIST) { + const taskRepo = entityManager.getRepository(Task); + // 削除対象ユーザーが文字起こし担当のタスクが存在するか確認 + const transcriptionTasksExist = await taskRepo.exist({ + where: { + account_id: target.account_id, + typist_user_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, // lockする事で状態遷移の競合をブロックし、新規追加以外で所有タスク群の状態変更を防ぐ + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // 削除対象のユーザーが文字起こし担当のタスクが存在する場合は削除できない + if (transcriptionTasksExist) { + throw new ExistsValidCheckoutDeleteFailedError('User has tasks.'); + } + + const checkoutPermissionRepo = + entityManager.getRepository(CheckoutPermission); + const permissions = await checkoutPermissionRepo.find({ + where: { + user_id: target.id, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // タスクのチェックアウト権限が残っていたら削除できない + if (permissions.length !== 0) { + throw new ExistsValidCheckoutDeleteFailedError( + 'User has checkout permissions.', + ); + } + } + // 対象ユーザーのライセンス割り当て状態を取得 + const licenseRepo = entityManager.getRepository(License); + const allocatedLicense = await licenseRepo.findOne({ + where: { + allocated_user_id: userId, + }, + lock: { mode: 'pessimistic_write' }, + comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, + }); + + // ライセンスが割り当て済みかつ、 + if (allocatedLicense !== null) { + const { status, expiry_date } = allocatedLicense; + // 有効な状態かつ、 + if (status === LICENSE_ALLOCATED_STATUS.ALLOCATED) { + // 有効期限を迎えていない場合は削除できない + if (expiry_date !== null && expiry_date > currentTime) { + throw new ExistsValidLicenseDeleteFailedError( + 'User has valid licenses.', + ); + } + } + } + + // ユーザーを削除する前に、削除対象ユーザーをアーカイブする + const userArchiveRepo = entityManager.getRepository(UserArchive); + await insertEntity( + UserArchive, + userArchiveRepo, + target, + this.isCommentOut, + context, + ); + // 期限切れライセンスが割り当てられていた場合、ユーザーを削除する前にライセンスを割り当て解除する + // ※この処理時点で有効期限外ライセンスであることは確定であるため、期限切れ判定をここでは行わない + if (allocatedLicense != null) { + allocatedLicense.status = LICENSE_ALLOCATED_STATUS.REUSABLE; + allocatedLicense.allocated_user_id = null; + + await updateEntity( + licenseRepo, + { id: allocatedLicense.id }, + allocatedLicense, + this.isCommentOut, + context, + ); + + // ライセンス割り当て履歴テーブルへ登録 + const licenseAllocationHistoryRepo = entityManager.getRepository( + LicenseAllocationHistory, + ); + const deallocationHistory = new LicenseAllocationHistory(); + deallocationHistory.user_id = userId; + deallocationHistory.license_id = allocatedLicense.id; + deallocationHistory.account_id = account.id; + deallocationHistory.is_allocated = false; + deallocationHistory.executed_at = new Date(); + deallocationHistory.switch_from_type = SWITCH_FROM_TYPE.NONE; + await insertEntity( + LicenseAllocationHistory, + licenseAllocationHistoryRepo, + deallocationHistory, + this.isCommentOut, + context, + ); + } + // ユーザテーブルのレコードを削除する + await deleteEntity( + userRepo, + { id: target.id }, + this.isCommentOut, + context, + ); + // ソート条件のテーブルのレコードを削除する + const sortCriteriaRepo = entityManager.getRepository(SortCriteria); + await deleteEntity( + sortCriteriaRepo, + { user_id: target.id }, + this.isCommentOut, + context, + ); + return { isSuccess: true }; + }); + } + /** * UserID指定のユーザーとソート条件を同時に削除する * @param userId diff --git a/dictation_server/src/templates/constants.ts b/dictation_server/src/templates/constants.ts index 72e8fc0..47ebb6d 100644 --- a/dictation_server/src/templates/constants.ts +++ b/dictation_server/src/templates/constants.ts @@ -11,3 +11,19 @@ export const AUTHOR_NAME = '$AUTHOR_NAME$'; export const FILE_NAME = '$FILE_NAME$'; export const TYPIST_NAME = '$TYPIST_NAME$'; export const TEMPORARY_PASSWORD = '$TEMPORARY_PASSWORD$'; +export const REQUEST_TIME = '$REQUEST_TIME$'; +// 言語ごとに変更 +export const EMAIL_DUPLICATION_EN = `$EMAIL_DUPLICATION_EN$`; +export const AUTHOR_ID_DUPLICATION_EN = `$AUTHOR_ID_DUPLICATION_EN$`; +export const UNEXPECTED_ERROR_EN = `$UNEXPECTED_ERROR_EN$`; +export const EMAIL_DUPLICATION_DE = `$EMAIL_DUPLICATION_DE$`; +export const AUTHOR_ID_DUPLICATION_DE = `$AUTHOR_ID_DUPLICATION_DE$`; +export const UNEXPECTED_ERROR_DE = `$UNEXPECTED_ERROR_DE$`; +export const EMAIL_DUPLICATION_FR = `$EMAIL_DUPLICATION_FR$`; +export const AUTHOR_ID_DUPLICATION_FR = `$AUTHOR_ID_DUPLICATION_FR$`; +export const UNEXPECTED_ERROR_FR = `$UNEXPECTED_ERROR_FR$`; + +// 言語ごとに当てはまる値 +export const NO_ERROR_MESSAGE_EN = 'No errors'; +export const NO_ERROR_MESSAGE_DE = 'Keine Fehler'; +export const NO_ERROR_MESSAGE_FR = 'Aucune erreur'; diff --git a/dictation_server/src/templates/template_U_111.html b/dictation_server/src/templates/template_U_111.html index ae7fb1d..b074cfd 100644 --- a/dictation_server/src/templates/template_U_111.html +++ b/dictation_server/src/templates/template_U_111.html @@ -1,69 +1,68 @@ - - Account Deleted Notification [U-111] - - -
    -

    <English>

    -

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    -

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

    -

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

    -

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

    -
    -
    -

    <Deutsch>

    -

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

    -

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

    -

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

    -

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

    -

    -
    -
    -

    <Français>

    -

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

    -

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

    -

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

    -

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

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

    <English>

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    Dear $USER_NAME$,

    +

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

    +

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

    +

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

    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $USER_NAME$,

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    Chère/Cher $USER_NAME$,

    +

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

    +

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

    +

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

    +
    + + diff --git a/dictation_server/src/templates/template_U_116.txt b/dictation_server/src/templates/template_U_116.txt new file mode 100644 index 0000000..0b00153 --- /dev/null +++ b/dictation_server/src/templates/template_U_116.txt @@ -0,0 +1,32 @@ + + +Dear $USER_NAME$, + +Thank you for using ODMS Cloud. Your user information has been deleted from ODMS Cloud. + +If you need support regarding ODMS Cloud, please contact $PRIMARY_ADMIN_NAME$. + +If you have received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $USER_NAME$, + +Vielen Dank, dass Sie ODMS Cloud nutzen. Ihre Benutzerinformationen wurden aus der ODMS Cloud gelöscht. + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $PRIMARY_ADMIN_NAME$. + +Wenn Sie diese E-Mail fälschlicherweise erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und dieses Postfach wird nicht überwacht. Bitte nicht antworten. + + + +Chère/Cher $USER_NAME$, + +Merci d'utiliser ODMS Cloud. Vos informations utilisateur ont été supprimées d'ODMS Cloud. + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $PRIMARY_ADMIN_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_118.html b/dictation_server/src/templates/template_U_118.html new file mode 100644 index 0000000..acad01a --- /dev/null +++ b/dictation_server/src/templates/template_U_118.html @@ -0,0 +1,94 @@ + + + Storage Usage Worning Notification [U-118] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    Dear $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    Sehr geehrte(r) $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    Chère/Cher $CUSTOMER_NAME$,

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120.txt b/dictation_server/src/templates/template_U_120.txt new file mode 100644 index 0000000..2d856d9 --- /dev/null +++ b/dictation_server/src/templates/template_U_120.txt @@ -0,0 +1,50 @@ + + +Dear $CUSTOMER_NAME$, + +We have received your bulk user registration request. + - Date and time: $REQUEST_TIME$ + - CSV file name: $FILE_NAME$ + +・Please wait until the registration is complete. This may take a few minutes to process. +・Notification will be sent separately upon completion. +・Registration cannot be completed if there are invalid values in the CSV file. + +If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. + - Datum und Uhrzeit: $REQUEST_TIME$ + - CSV-Dateiname: $FILE_NAME$ + +・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. +・Eine Benachrichtigung wird nach Abschluss separat verschickt. +・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, + +Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier CSV : $FILE_NAME$ + +・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. +・Une notification sera envoyée séparément une fois terminée. +・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120_no_parent.html b/dictation_server/src/templates/template_U_120_no_parent.html new file mode 100644 index 0000000..ae319b0 --- /dev/null +++ b/dictation_server/src/templates/template_U_120_no_parent.html @@ -0,0 +1,71 @@ + + + + User Bulk Registration Received Notification [U-120] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_120_no_parent.txt b/dictation_server/src/templates/template_U_120_no_parent.txt new file mode 100644 index 0000000..4e46961 --- /dev/null +++ b/dictation_server/src/templates/template_U_120_no_parent.txt @@ -0,0 +1,44 @@ + + +Dear $CUSTOMER_NAME$, + +We have received your bulk user registration request. + - Date and time: $REQUEST_TIME$ + - CSV file name: $FILE_NAME$ + +・Please wait until the registration is complete. This may take a few minutes to process. +・Notification will be sent separately upon completion. +・Registration cannot be completed if there are invalid values in the CSV file. + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Wir haben Ihre Anfrage zur Massenbenutzerregistrierung erhalten. + - Datum und Uhrzeit: $REQUEST_TIME$ + - CSV-Dateiname: $FILE_NAME$ + +・Bitte warten Sie, bis die Registrierung abgeschlossen ist. Die Bearbeitung kann einige Minuten dauern. +・Eine Benachrichtigung wird nach Abschluss separat verschickt. +・Die Registrierung kann nicht abgeschlossen werden, wenn die CSV-Datei ungültige Werte enthält. + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, + +Nous avons reçu votre demande d'enregistrement groupé d'utilisateur. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier CSV : $FILE_NAME$ + +・Veuillez attendre que l'inscription soit terminée. Le traitement peut prendre quelques minutes. +・Une notification sera envoyée séparément une fois terminée. +・L'inscription ne peut pas être complétée s'il y a des valeurs invalides dans le fichier CSV. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121.html b/dictation_server/src/templates/template_U_121.html new file mode 100644 index 0000000..e2eac21 --- /dev/null +++ b/dictation_server/src/templates/template_U_121.html @@ -0,0 +1,80 @@ + + + + User Bulk Registration Completed Notification [U-121] + + + +
    +

    <English>

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121.txt b/dictation_server/src/templates/template_U_121.txt new file mode 100644 index 0000000..b04a729 --- /dev/null +++ b/dictation_server/src/templates/template_U_121.txt @@ -0,0 +1,50 @@ + + +Dear $CUSTOMER_NAME$, + +Bulk user registration using the CSV file has been completed. + - Date and time: $REQUEST_TIME$ + - CSV file name: $FILE_NAME$ + +・User Registration Notification [U-114] will be sent to the registered users. +・Registration will not be completed unless the user verifies their email address. +・You can check the verification status of each user from the [User] tab in the ODMS Cloud. + +If you need support regarding the ODMS Cloud, please contact $DEALER_NAME$. + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. + - Datum und Uhrzeit: $REQUEST_TIME$ + - CSV-Dateiname: $FILE_NAME$ + +・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. +・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. +・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud überprüfen. + +Wenn Sie Unterstützung bezüglich ODMS Cloud benötigen, wenden Sie sich bitte an $DEALER_NAME$. + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier CSV : $FILE_NAME$ + +・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. +・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. +・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le cloud ODMS. + +Si vous avez besoin d'assistance concernant ODMS Cloud, veuillez contacter $DEALER_NAME$. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121_no_parent.html b/dictation_server/src/templates/template_U_121_no_parent.html new file mode 100644 index 0000000..2f52ad9 --- /dev/null +++ b/dictation_server/src/templates/template_U_121_no_parent.html @@ -0,0 +1,71 @@ + + + + User Bulk Registration Completed Notification [U-121] + + + +
    +

    <English>

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_121_no_parent.txt b/dictation_server/src/templates/template_U_121_no_parent.txt new file mode 100644 index 0000000..650d6b7 --- /dev/null +++ b/dictation_server/src/templates/template_U_121_no_parent.txt @@ -0,0 +1,44 @@ + + +Dear $CUSTOMER_NAME$, + +Bulk user registration using the CSV file has been completed. + - Date and time: $REQUEST_TIME$ + - CSV file name: $FILE_NAME$ + +・User Registration Notification [U-114] will be sent to the registered users. +・Registration will not be completed unless the user verifies their email address. +・You can check the verification status of each user from the [User] tab in the ODMS Cloud. + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Massenbenutzerregistrierung mithilfe der CSV-Datei wurde abgeschlossen. + - Datum und Uhrzeit: $REQUEST_TIME$ + - CSV-Dateiname: $FILE_NAME$ + +・Die Benutzerregistrierungsbenachrichtigung [U-114] wird an die registrierten Benutzer gesendet. +・Die Registrierung wird erst abgeschlossen, wenn der Benutzer seine E-Mail-Adresse bestätigt. +・Sie können den Verifizierungsstatus jedes Benutzers auf der Registerkarte [Benutzer] in der ODMS Cloud überprüfen. + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV est terminé. + - Date et heure : $REQUEST_TIME$ + - Nom du fichier CSV : $FILE_NAME$ + +・La notification d'enregistrement de l'utilisateur [U-114] sera envoyée aux utilisateurs enregistrés. +・L'inscription ne sera complétée que si l'utilisateur vérifie son adresse e-mail. +・Vous pouvez vérifier le statut de vérification de chaque utilisateur à partir de l'onglet [Utilisateur] dans le cloud ODMS. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122.html b/dictation_server/src/templates/template_U_122.html new file mode 100644 index 0000000..b456eb5 --- /dev/null +++ b/dictation_server/src/templates/template_U_122.html @@ -0,0 +1,137 @@ + + + + User Bulk Registration Failed Notification [U-122] + + + +
    +

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

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

    <English>

    +

    + Dear $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

    + Sehr geehrte(r) $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

    + Chère/Cher $CUSTOMER_NAME$, +

    +

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

    +

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

    +

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

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_122_no_parent.txt b/dictation_server/src/templates/template_U_122_no_parent.txt new file mode 100644 index 0000000..f9b3461 --- /dev/null +++ b/dictation_server/src/templates/template_U_122_no_parent.txt @@ -0,0 +1,62 @@ + + +Dear $CUSTOMER_NAME$, + +Bulk user registration using the CSV file has failed. The cause and location of the error is shown in 1, 2, and 3 below. ( L = Line ) + + 1. The e-mail address in the line below has already been registered or is a duplicate of an e-mail address in another line. + $EMAIL_DUPLICATION_EN$ + + 2. The Author ID in the line below is already registered or is a duplicate of an Author ID in another line. + $AUTHOR_ID_DUPLICATION_EN$ + + * E-mail address and Author ID that have already been registered cannot be registered again. + * Rows without errors have been successfully registered. Therefore, if you use the same CSV file and register the user that has been successfully registered, a duplicate error will occur. Please create a CSV file containing only the lines where the error occurred, or manually register them one by one. + + 3. An unexpected error occurred during user registration on the following line. If it does not succeed after trying again, please contact your dealer. +   $UNEXPECTED_ERROR_EN$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, + +Die Massenregistrierung von Benutzern mithilfe der CSV-Datei ist fehlgeschlagen. Die Ursache und der Ort des Fehlers werden in 1, 2 und 3 unten angezeigt. (L = Linie) + + 1. Die E-Mail-Adresse in der Zeile unten ist bereits registriert oder ist ein Duplikat einer E-Mail-Adresse in einer anderen Zeile. + $EMAIL_DUPLICATION_DE$ + + 2. Die Author-ID in der Zeile darunter ist bereits registriert oder ein Duplikat einer AuthorID in einer anderen Zeile. + $AUTHOR_ID_DUPLICATION_DE$ + + * E-Mail-Adresse und Autoren-ID, die bereits registriert wurden, können nicht erneut registriert werden. + * Zeilen ohne Fehler wurden erfolgreich registriert. Wenn Sie daher dieselbe CSV-Datei verwenden und den erfolgreich registrierten Benutzer registrieren, tritt ein doppelter Fehler auf. Bitte erstellen Sie eine CSV-Datei, die nur die Zeilen enthält, in denen der Fehler aufgetreten ist, oder registrieren Sie sie einzeln manuell. + + 3. Bei der Benutzerregistrierung ist in der folgenden Zeile ein unerwarteter Fehler aufgetreten. Sollte es auch nach einem erneuten Versuch nicht erfolgreich sein, wenden Sie sich bitte an Ihren Händler. + $UNEXPECTED_ERROR_DE$ + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, + +L'enregistrement groupé des utilisateurs à l'aide du fichier CSV a échoué. La cause et l'emplacement de l'erreur sont indiqués aux points 1, 2 et 3 ci-dessous. ( L = Ligne ) + + 1. L'adresse e-mail dans la ligne ci-dessous a déjà été enregistrée ou est un double d'une adresse e-mail dans une autre ligne. + $EMAIL_DUPLICATION_FR$ + + 2. L'Identifiant Auteur dans la ligne ci-dessous est déjà enregistré ou est un double d'un Identifiant Auteur dans une autre ligne. + $AUTHOR_ID_DUPLICATION_FR$ + + * L'adresse e-mail et l'Identifiant Auteur déjà enregistrés ne peuvent pas être enregistrés à nouveau. + * Les lignes sans erreurs ont été enregistrées avec succès. Par conséquent, si vous utilisez le même fichier CSV et enregistrez l'utilisateur qui a été enregistré avec succès, une erreur en double se produira. Veuillez créer un fichier CSV contenant uniquement les lignes où l'erreur s'est produite, ou enregistrez-les manuellement une par une. + + 3. Une erreur inattendue s'est produite lors de l'enregistrement de l'utilisateur sur la ligne suivante. Si cela ne fonctionne pas après une nouvelle tentative, veuillez contacter votre revendeur. + $UNEXPECTED_ERROR_FR$ + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_123.html b/dictation_server/src/templates/template_U_123.html new file mode 100644 index 0000000..86c1e5b --- /dev/null +++ b/dictation_server/src/templates/template_U_123.html @@ -0,0 +1,64 @@ + + + + Partner Account Deleted Notification [U-123] + + + +
    +

    <English>

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

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

    +

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

    +

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

    +

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

    +
    +
    +

    <Français>

    +

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

    +

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

    +

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

    +

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

    +
    + + + \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_123.txt b/dictation_server/src/templates/template_U_123.txt new file mode 100644 index 0000000..ea9f994 --- /dev/null +++ b/dictation_server/src/templates/template_U_123.txt @@ -0,0 +1,32 @@ + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Your account information has been removed from the ODMS Cloud by $DEALER_NAME$. + +If you would like to register your account with the ODMS Cloud again, please contact your $DEALER_NAME$ or contact OM System directly. + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Ihre Kontoinformationen wurden von $DEALER_NAME$ aus der ODMS Cloud entfernt. + +Wenn Sie Ihr Konto erneut bei der ODMS Cloud registrieren möchten, wenden Sie sich bitte an Ihren $DEALER_NAME$ oder wenden Sie sich direkt an OM System. + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Les informations de votre compte ont été supprimées du cloud ODMS par $DEALER_NAME$. + +Si vous souhaitez enregistrer à nouveau votre compte sur ODMS Cloud, veuillez contacter votre $DEALER_NAME$ ou contacter directement OM System. + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file diff --git a/dictation_server/src/templates/template_U_124.html b/dictation_server/src/templates/template_U_124.html new file mode 100644 index 0000000..98d7236 --- /dev/null +++ b/dictation_server/src/templates/template_U_124.html @@ -0,0 +1,58 @@ + + + Storage Usage Exceeded Notification [U-119] + + + +
    +

    <English>

    +

    Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$

    +

    Your account information has been edited by $DEALER_NAME$.

    +

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

    +

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

    +
    +
    +

    <Deutsch>

    +

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

    +

    Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet.

    +

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

    +

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

    +
    +
    +

    <Français>

    +

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

    +

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

    +

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

    +

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

    +
    + + diff --git a/dictation_server/src/templates/template_U_124.txt b/dictation_server/src/templates/template_U_124.txt new file mode 100644 index 0000000..e12dc2c --- /dev/null +++ b/dictation_server/src/templates/template_U_124.txt @@ -0,0 +1,35 @@ + + +Dear $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Your account information has been edited by $DEALER_NAME$. + +To check or change your account information, please log in to the ODMS Cloud. +URL: $TOP_URL$ + +If you received this e-mail in error, please delete this e-mail from your system. +This is an automatically generated e-mail and this mailbox is not monitored. Please do not reply. + + + +Sehr geehrte(r) $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Ihre Kontoinformationen wurden von $DEALER_NAME$ bearbeitet. + +Um Ihre Kontoinformationen zu überprüfen oder zu ändern, melden Sie sich bitte bei der ODMS Cloud an. +URL: $TOP_URL$ + +Wenn Sie diese E-Mail irrtümlich erhalten haben, löschen Sie diese E-Mail bitte aus Ihrem System. +Dies ist eine automatisch generierte E-Mail und diese Mailbox wird nicht überwacht. Bitte antworten Sie nicht. + + + +Chère/Cher $CUSTOMER_NAME$, -> $PRIMARY_ADMIN_NAME$ + +Les informations de votre compte ont été modifiées par $DEALER_NAME$. + +Pour vérifier ou modifier les informations de votre compte, veuillez vous connecter au ODMS Cloud. +URL : $TOP_URL$ + +Si vous avez reçu cet e-mail par erreur, veuillez supprimer cet e-mail de votre système. +Il s'agit d'un e-mail généré automatiquement et cette boîte aux lettres n'est pas surveillée. Merci de ne pas répondre. \ No newline at end of file
    {user.name}